Просмотр исходного кода

Merge branch 'master' into pr-aggregate-insert-delete

Paulo Reis 9 лет назад
Родитель
Сommit
19ecffd654
34 измененных файлов с 460 добавлено и 206 удалено
  1. 1 1
      services/web/app/coffee/Features/Editor/EditorController.coffee
  2. 12 0
      services/web/app/coffee/Features/Errors/ErrorController.coffee
  3. 7 2
      services/web/app/coffee/Features/Errors/Errors.coffee
  4. 7 15
      services/web/app/coffee/Features/Project/ProjectController.coffee
  5. 16 13
      services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
  6. 23 11
      services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee
  7. 0 1
      services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee
  8. 1 1
      services/web/app/coffee/Features/Uploads/FileTypeManager.coffee
  9. 2 1
      services/web/app/coffee/infrastructure/Server.coffee
  10. 2 0
      services/web/app/views/project/editor/left-menu.pug
  11. 10 2
      services/web/app/views/project/list/modals.pug
  12. 73 70
      services/web/app/views/sentry.pug
  13. 10 2
      services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee
  14. 1 0
      services/web/public/coffee/ide/editor/Document.coffee
  15. 4 2
      services/web/public/coffee/ide/editor/directives/aceEditor.coffee
  16. 44 12
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
  17. 64 0
      services/web/public/coffee/ide/editor/directives/aceEditor/labels/LabelsManager.coffee
  18. 53 0
      services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee
  19. 8 2
      services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee
  20. 3 7
      services/web/public/coffee/ide/settings/services/settings.coffee
  21. 41 7
      services/web/public/coffee/main/project-list/modal-controllers.coffee
  22. 7 28
      services/web/public/coffee/main/project-list/project-list.coffee
  23. 13 8
      services/web/public/coffee/services/queued-http.coffee
  24. 1 0
      services/web/public/js/libs/raven-3.15.0.min.js
  25. 4 5
      services/web/public/stylesheets/app/editor.less
  26. 1 1
      services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee
  27. 5 13
      services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee
  28. 15 0
      services/web/test/UnitTests/coffee/Project/ProjectCreationHandlerTests.coffee
  29. 29 0
      services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee
  30. 1 0
      services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee
  31. 0 1
      services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee
  32. 1 0
      services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee
  33. 1 0
      services/web/test/UnitTests/coffee/User/UserLocatorTests.coffee
  34. 0 1
      services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee

+ 1 - 1
services/web/app/coffee/Features/Editor/EditorController.coffee

@@ -176,7 +176,7 @@ module.exports = EditorController =
 					callback?()
 
 	renameProject: (project_id, newName, callback = (err) ->) ->
-		ProjectDetailsHandler.renameProject project_id, newName, ->
+		ProjectDetailsHandler.renameProject project_id, newName, (err) ->
 			if err?
 				logger.err err:err, project_id:project_id, newName:newName, "error renaming project"
 				return callback(err)

+ 12 - 0
services/web/app/coffee/Features/Errors/ErrorController.coffee

@@ -25,6 +25,18 @@ module.exports = ErrorController =
 		else if error instanceof Errors.TooManyRequestsError
 			logger.warn {err: error, url: req.url}, "too many requests error"
 			res.sendStatus(429)
+		else if error instanceof Errors.InvalidNameError
+			logger.warn {err: error, url: req.url}, "invalid name error"
+			res.status(400)
+			res.send(error.message)
 		else
 			logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear"
 			ErrorController.serverError req, res
+
+	handleApiError: (error, req, res, next) ->
+		if error instanceof Errors.NotFoundError
+			logger.warn {err: error, url: req.url}, "not found error"
+			res.sendStatus(404)
+		else
+			logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear"
+			res.sendStatus(500)

+ 7 - 2
services/web/app/coffee/Features/Errors/Errors.coffee

@@ -5,7 +5,6 @@ NotFoundError = (message) ->
 	return error
 NotFoundError.prototype.__proto__ = Error.prototype
 
-
 ServiceNotConfiguredError = (message) ->
 	error = new Error(message)
 	error.name = "ServiceNotConfiguredError"
@@ -13,7 +12,6 @@ ServiceNotConfiguredError = (message) ->
 	return error
 ServiceNotConfiguredError.prototype.__proto__ = Error.prototype
 
-
 TooManyRequestsError = (message) ->
 	error = new Error(message)
 	error.name = "TooManyRequestsError"
@@ -21,8 +19,15 @@ TooManyRequestsError = (message) ->
 	return error
 TooManyRequestsError.prototype.__proto__ = Error.prototype
 
+InvalidNameError = (message) ->
+	error = new Error(message)
+	error.name = "InvalidNameError"
+	error.__proto__ = InvalidNameError.prototype
+	return error
+InvalidNameError.prototype.__proto__ = Error.prototype
 
 module.exports = Errors =
 	NotFoundError: NotFoundError
 	ServiceNotConfiguredError: ServiceNotConfiguredError
 	TooManyRequestsError: TooManyRequestsError
+	InvalidNameError: InvalidNameError

+ 7 - 15
services/web/app/coffee/Features/Project/ProjectController.coffee

@@ -101,7 +101,7 @@ module.exports = ProjectController =
 			res.send(project_id:project._id)
 
 
-	newProject: (req, res)->
+	newProject: (req, res, next)->
 		user_id = AuthenticationController.getLoggedInUserId(req)
 		projectName = req.body.projectName?.trim()
 		template = req.body.template
@@ -113,25 +113,17 @@ module.exports = ProjectController =
 				else
 					projectCreationHandler.createBasicProject user_id, projectName, cb
 		], (err, project)->
-			if err?
-				logger.error err: err, project: project, user: user_id, name: projectName, templateType: template, "error creating project"
-				res.sendStatus 500
-			else
-				logger.log project: project, user: user_id, name: projectName, templateType: template, "created project"
-				res.send {project_id:project._id}
+			return next(err) if err?
+			logger.log project: project, user: user_id, name: projectName, templateType: template, "created project"
+			res.send {project_id:project._id}
 
 
-	renameProject: (req, res)->
+	renameProject: (req, res, next)->
 		project_id = req.params.Project_id
 		newName = req.body.newProjectName
-		if newName.length > 150
-			return res.sendStatus 400
 		editorController.renameProject project_id, newName, (err)->
-			if err?
-				logger.err err:err, project_id:project_id, newName:newName, "problem renaming project"
-				res.sendStatus 500
-			else
-				res.sendStatus 200
+			return next(err) if err?
+			res.sendStatus 200
 
 	projectListPage: (req, res, next)->
 		timer = new metrics.Timer("project-list")

+ 16 - 13
services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee

@@ -6,6 +6,7 @@ ObjectId = require('mongoose').Types.ObjectId
 Project = require('../../models/Project').Project
 Folder = require('../../models/Folder').Folder
 ProjectEntityHandler = require('./ProjectEntityHandler')
+ProjectDetailsHandler = require('./ProjectDetailsHandler')
 User = require('../../models/User').User
 fs = require('fs')
 Path = require "path"
@@ -15,19 +16,21 @@ module.exports = ProjectCreationHandler =
 
 	createBlankProject : (owner_id, projectName, callback = (error, project) ->)->
 		metrics.inc("project-creation")
-		logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
-		rootFolder = new Folder {'name':'rootFolder'}
-		project = new Project
-			 owner_ref  : new ObjectId(owner_id)
-			 name       : projectName
-		if Settings.currentImageName?
-			project.imageName = Settings.currentImageName
-		project.rootFolder[0] = rootFolder
-		User.findById owner_id, "ace.spellCheckLanguage", (err, user)->
-			project.spellCheckLanguage = user.ace.spellCheckLanguage
-			project.save (err)->
-				return callback(err) if err?
-				callback err, project
+		ProjectDetailsHandler.validateProjectName projectName, (error) ->
+			return callback(error) if error?
+			logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
+			rootFolder = new Folder {'name':'rootFolder'}
+			project = new Project
+				 owner_ref  : new ObjectId(owner_id)
+				 name       : projectName
+			if Settings.currentImageName?
+				project.imageName = Settings.currentImageName
+			project.rootFolder[0] = rootFolder
+			User.findById owner_id, "ace.spellCheckLanguage", (err, user)->
+				project.spellCheckLanguage = user.ace.spellCheckLanguage
+				project.save (err)->
+					return callback(err) if err?
+					callback err, project
 
 	createBasicProject :  (owner_id, projectName, callback = (error, project) ->)->
 		self = @

+ 23 - 11
services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee

@@ -7,8 +7,7 @@ _ = require("underscore")
 PublicAccessLevels = require("../Authorization/PublicAccessLevels")
 Errors = require("../Errors/Errors")
 
-module.exports = 
-
+module.exports = ProjectDetailsHandler =
 	getDetails: (project_id, callback)->
 		ProjectGetter.getProject project_id, {name:true, description:true, compiler:true, features:true, owner_ref:true}, (err, project)->
 			if err?
@@ -39,16 +38,29 @@ module.exports =
 			callback(err)
 
 	renameProject: (project_id, newName, callback = ->)->
-		logger.log project_id: project_id, newName:newName, "renaming project"
-		ProjectGetter.getProject project_id, {name:true}, (err, project)->
-			if err? or !project?
-				logger.err err:err,  project_id:project_id, "error getting project or could not find it todo project rename"
-				return callback(err)
-			oldProjectName = project.name
-			Project.update _id:project_id, {name: newName}, (err, project)=>
-				if err?
+		ProjectDetailsHandler.validateProjectName newName, (error) ->
+			return callback(error) if error?
+			logger.log project_id: project_id, newName:newName, "renaming project"
+			ProjectGetter.getProject project_id, {name:true}, (err, project)->
+				if err? or !project?
+					logger.err err:err,  project_id:project_id, "error getting project or could not find it todo project rename"
 					return callback(err)
-				tpdsUpdateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newName}, callback
+				oldProjectName = project.name
+				Project.update _id:project_id, {name: newName}, (err, project)=>
+					if err?
+						return callback(err)
+					tpdsUpdateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newName}, callback
+
+	MAX_PROJECT_NAME_LENGTH: 150
+	validateProjectName: (name, callback = (error) ->) ->
+		if name.length == 0
+			return callback(new Errors.InvalidNameError("Project name cannot be blank"))
+		else if name.length > @MAX_PROJECT_NAME_LENGTH
+			return callback(new Errors.InvalidNameError("Project name is too long"))
+		else if name.indexOf("/") > -1
+			return callback(new Errors.InvalidNameError("Project name cannot not contain / characters"))
+		else
+			return callback()
 
 	setPublicAccessLevel : (project_id, newAccessLevel, callback = ->)->
 		logger.log project_id: project_id, level: newAccessLevel, "set public access level"

+ 0 - 1
services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee

@@ -1,6 +1,5 @@
 Project = require('../../models/Project').Project
 logger = require('logger-sharelatex')
-Project = require("../../models/Project").Project
 
 module.exports = 
 	markAsUpdated : (project_id, callback)->

+ 1 - 1
services/web/app/coffee/Features/Uploads/FileTypeManager.coffee

@@ -3,7 +3,7 @@ Path = require("path")
 
 module.exports = FileTypeManager =
 	TEXT_EXTENSIONS : [
-		"tex", "latex", "sty", "cls", "bst", "bib", "bibtex", "txt", "tikz", "rtex", "md"
+		"tex", "latex", "sty", "cls", "bst", "bib", "bibtex", "txt", "tikz", "rtex", "md", "asy"
 	]
 
 	IGNORE_EXTENSIONS : [

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

@@ -164,11 +164,12 @@ server = require('http').createServer(app)
 # process api routes first, if nothing matched fall though and use
 # web middlewear + routes
 app.use(apiRouter)
+app.use(ErrorController.handleApiError)
 app.use(webRouter)
+app.use(ErrorController.handleError)
 
 router = new Router(webRouter, apiRouter)
 
-app.use ErrorController.handleError
 
 module.exports =
 	app: app

+ 2 - 0
services/web/app/views/project/editor/left-menu.pug

@@ -167,6 +167,8 @@ script(type='text/ng-template', id='cloneProjectModalTemplate')
 	.modal-header
 		h3 #{translate("copy_project")}
 	.modal-body
+		.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} 
+		.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
 		form(name="cloneProjectForm", novalidate)
 			.form-group
 				label #{translate("new_name")}

+ 10 - 2
services/web/app/views/project/list/modals.pug

@@ -98,6 +98,8 @@ script(type='text/ng-template', id='renameProjectModalTemplate')
 		) ×
 		h3 #{translate("rename_project")}
 	.modal-body
+		.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} 
+		.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
 		form(name="renameProjectForm", novalidate)
 			input.form-control(
 				type="text",
@@ -111,8 +113,10 @@ script(type='text/ng-template', id='renameProjectModalTemplate')
 		button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
 		button.btn.btn-primary(
 			ng-click="rename()",
-			ng-disabled="renameProjectForm.$invalid"
-		) #{translate("rename")}
+			ng-disabled="renameProjectForm.$invalid || state.inflight"
+		)
+			span(ng-show="!state.inflight") #{translate("rename")}
+			span(ng-show="state.inflight") #{translate("renaming")}...
 
 script(type='text/ng-template', id='cloneProjectModalTemplate')
 	.modal-header
@@ -123,6 +127,8 @@ script(type='text/ng-template', id='cloneProjectModalTemplate')
 		) ×
 		h3 #{translate("copy_project")}
 	.modal-body
+		.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} 
+		.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
 		form(name="cloneProjectForm", novalidate)
 			.form-group
 				label #{translate("new_name")}
@@ -155,6 +161,8 @@ script(type='text/ng-template', id='newProjectModalTemplate')
 		) ×
 		h3 #{translate("new_project")}
 	.modal-body
+		.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} 
+		.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
 		form(novalidate, name="newProjectForm")
 			input.form-control(
 				type="text",

+ 73 - 70
services/web/app/views/sentry.pug

@@ -1,75 +1,78 @@
-- if (typeof(sentrySrc) != "undefined")
-	- if (sentrySrc.match(/^([a-z]+:)?\/\//i))
-		script(src=sentrySrc)
-	- else
-		script(src=buildJsPath("libs/"+sentrySrc, {fingerprint:false}))
-- if (typeof(sentrySrc) != "undefined")
+- if (typeof(sentryPublicDSN) != "undefined")
 	script(type="text/javascript").
-		if (typeof(Raven) != "undefined" && Raven.config) {
-			Raven.config("#{sentryPublicDSN}", {
-			tags: { 'commit': '@@COMMIT@@', 'build': '@@RELEASE@@' },
-			release: '@@RELEASE@@',
-			// Ignore list based off: https://gist.github.com/1878283
-			ignoreErrors: [
-				'DealPly',
-				// Random plugins/extensions
-				'top.GLOBALS',
-				// See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
-				'originalCreateNotification',
-				'canvas.contentDocument',
-				'MyApp_RemoveAllHighlights',
-				'http://tt.epicplay.com',
-				'Can\'t find variable: ZiteReader',
-				'jigsaw is not defined',
-				'ComboSearch is not defined',
-				'http://loading.retry.widdit.com/',
-				'atomicFindClose',
-				// Facebook borked
-				'fb_xd_fragment',
-				// ISP optimizing proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha)
-				// See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
-				'bmi_SafeAddOnload',
-				'EBCallBackMessageReceived',
-				// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
-				'conduitPage',
-				'/NS_ERROR_NOT_CONNECTED/i',
-				"/Cannot read property 'row' of undefined/i",
-				'TypeError: start is undefined'
-			],
-			ignoreUrls: [
-				// Facebook flakiness
-				/graph\.facebook\.com/i,
-				// Facebook blocked
-				/connect\.facebook\.net\/en_US\/all\.js/i,
-				// Woopra flakiness
-				/eatdifferent\.com\.woopra-ns\.com/i,
-				/static\.woopra\.com\/js\/woopra\.js/i,
-				// Chrome extensions
-				/extensions\//i,
-				/^chrome:\/\//i,
-				// Other plugins
-				/127\.0\.0\.1:4001\/isrunning/i,  // Cacaoweb
-				/webappstoolbarba\.texthelp\.com\//i,
-				/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
-				/a\.disquscdn\.com/i,
-				/platform\.twitter\.com/i,
-				/pstatic\.datafastguru\.info/i
-			],
-			shouldSendCallback: function(data) {
-				// only send a fraction of errors
-				var sampleRate = 0.01;
-				return (Math.random() <= sampleRate);
-			},
-			dataCallback: function(data) {
-				// remove circular references from object
-				var cache = [];
-				var s = JSON.stringify(data, function(k, v) { if (typeof v === 'object' && v !== null) { if (cache.indexOf(v) !== -1) return "[circular]"; cache.push(v); }; return v; });
-				return JSON.parse(s);
+		require.config({
+			paths: {
+				'raven': 'libs/raven-3.15.0.min'
 			}
-			// we highly recommend restricting exceptions to a domain in order to filter out clutter
-			// whitelistUrls: ['example.com/scripts/']
-			}).install();
-		}
+		});
+
+		require(["raven"], function(Raven) {
+			if (typeof(Raven) != "undefined" && Raven.config) {
+				Raven.config("#{sentryPublicDSN}", {
+				tags: { 'commit': '@@COMMIT@@', 'build': '@@RELEASE@@' },
+				release: '@@RELEASE@@',
+				// Ignore list based off: https://gist.github.com/1878283
+				ignoreErrors: [
+					'DealPly',
+					// Random plugins/extensions
+					'top.GLOBALS',
+					// See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
+					'originalCreateNotification',
+					'canvas.contentDocument',
+					'MyApp_RemoveAllHighlights',
+					'http://tt.epicplay.com',
+					'Can\'t find variable: ZiteReader',
+					'jigsaw is not defined',
+					'ComboSearch is not defined',
+					'http://loading.retry.widdit.com/',
+					'atomicFindClose',
+					// Facebook borked
+					'fb_xd_fragment',
+					// ISP optimizing proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha)
+					// See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
+					'bmi_SafeAddOnload',
+					'EBCallBackMessageReceived',
+					// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
+					'conduitPage',
+					'/NS_ERROR_NOT_CONNECTED/i',
+					"/Cannot read property 'row' of undefined/i",
+					'TypeError: start is undefined'
+				],
+				ignoreUrls: [
+					// Facebook flakiness
+					/graph\.facebook\.com/i,
+					// Facebook blocked
+					/connect\.facebook\.net\/en_US\/all\.js/i,
+					// Woopra flakiness
+					/eatdifferent\.com\.woopra-ns\.com/i,
+					/static\.woopra\.com\/js\/woopra\.js/i,
+					// Chrome extensions
+					/extensions\//i,
+					/^chrome:\/\//i,
+					// Other plugins
+					/127\.0\.0\.1:4001\/isrunning/i,  // Cacaoweb
+					/webappstoolbarba\.texthelp\.com\//i,
+					/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
+					/a\.disquscdn\.com/i,
+					/platform\.twitter\.com/i,
+					/pstatic\.datafastguru\.info/i
+				],
+				shouldSendCallback: function(data) {
+					// only send a fraction of errors
+					var sampleRate = 0.01;
+					return (Math.random() <= sampleRate);
+				},
+				dataCallback: function(data) {
+					// remove circular references from object
+					var cache = [];
+					var s = JSON.stringify(data, function(k, v) { if (typeof v === 'object' && v !== null) { if (cache.indexOf(v) !== -1) return "[circular]"; cache.push(v); }; return v; });
+					return JSON.parse(s);
+				}
+				// we highly recommend restricting exceptions to a domain in order to filter out clutter
+				// whitelistUrls: ['example.com/scripts/']
+				}).install();
+			}
+		})
 	- if (user && typeof(user) != "undefined" && typeof (user.email) != "undefined")
 		script(type="text/javascript").
 			if (typeof(Raven) != "undefined" && Raven.setUserContext) {

+ 10 - 2
services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee

@@ -6,6 +6,7 @@ define [
 			projectName: ide.$scope.project.name + " (Copy)"
 		$scope.state =
 			inflight: false
+			error: false
 
 		$modalInstance.opened.then () ->
 			$timeout () ->
@@ -20,9 +21,16 @@ define [
 
 		$scope.clone = () ->
 			$scope.state.inflight = true
+			$scope.state.error = false
 			cloneProject($scope.inputs.projectName)
-				.then (data) ->
-					window.location = "/project/#{data.data.project_id}"
+				.success (data) ->
+					window.location = "/project/#{data.project_id}"
+				.error (body, statusCode) ->
+					$scope.state.inflight = false
+					if statusCode == 400
+						$scope.state.error = { message: body }
+					else
+						$scope.state.error = true
 
 		$scope.cancel = () ->
 			$modalInstance.dismiss('cancel')

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

@@ -35,6 +35,7 @@ define [
 			@doc?.attachToAce(@ace)
 			editorDoc = @ace.getSession().getDocument()
 			editorDoc.on "change", @_checkConsistency
+			@ide.$scope.$emit 'document:opened', @doc
 
 		detachFromAce: () ->
 			@doc?.detachFromAce()

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

@@ -9,7 +9,8 @@ define [
 	"ide/editor/directives/aceEditor/highlights/HighlightsManager"
 	"ide/editor/directives/aceEditor/cursor-position/CursorPositionManager"
 	"ide/editor/directives/aceEditor/track-changes/TrackChangesManager"
-], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager) ->
+	"ide/editor/directives/aceEditor/labels/LabelsManager"
+], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, LabelsManager) ->
 	EditSession = ace.require('ace/edit_session').EditSession
 	ModeList = ace.require('ace/ext/modelist')
 
@@ -84,7 +85,6 @@ define [
 
 				scope.name = attrs.aceEditor
 
-				autoCompleteManager   = new AutoCompleteManager(scope, editor, element)
 				if scope.spellCheck # only enable spellcheck when explicitly required
 					spellCheckCache =  $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000})
 					spellCheckManager = new SpellCheckManager(scope, editor, element, spellCheckCache)
@@ -92,6 +92,8 @@ define [
 				highlightsManager     = new HighlightsManager(scope, editor, element)
 				cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
 				trackChangesManager   = new TrackChangesManager(scope, editor, element)
+				labelsManager         = new LabelsManager(scope, editor, element)
+				autoCompleteManager   = new AutoCompleteManager(scope, editor, element, labelsManager)
 
 				# Prevert Ctrl|Cmd-S from triggering save dialog
 				editor.commands.addCommand

+ 44 - 12
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee

@@ -13,7 +13,7 @@ define [
 			return null
 
 	class AutoCompleteManager
-		constructor: (@$scope, @editor) ->
+		constructor: (@$scope, @editor, @element, @labelsManager) ->
 			@suggestionManager = new SuggestionManager()
 
 			@monkeyPatchAutocomplete()
@@ -40,6 +40,35 @@ define [
 
 			SnippetCompleter = new SnippetManager()
 
+			labelsManager = @labelsManager
+			LabelsCompleter =
+				getCompletions: (editor, session, pos, prefxi, callback) ->
+					upToCursorRange = new Range(pos.row, 0, pos.row, pos.column)
+					lineUpToCursor = editor.getSession().getTextRange(upToCursorRange)
+					commandFragment = getLastCommandFragment(lineUpToCursor)
+					if commandFragment
+						refMatch = commandFragment.match(/^~?\\ref{([^}]*, *)?(\w*)/)
+						if refMatch
+							beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999)
+							lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange)
+							needsClosingBrace = !lineBeyondCursor.match(/^[^{]*}/)
+							currentArg = refMatch[1]
+							result = []
+							result.push {
+								caption: "\\ref{}",
+								snippet: "\\ref{}",
+								meta: "cross-reference",
+								score: 11000
+							}
+							for label in labelsManager.getAllLabels()
+								result.push {
+									caption: "\\ref{#{label}#{if needsClosingBrace then '}' else ''}",
+									value: "\\ref{#{label}#{if needsClosingBrace then '}' else ''}",
+									meta: "cross-reference",
+									score: 10000
+								}
+							callback null, result
+
 			references = @$scope.$root._references
 			ReferencesCompleter =
 				getCompletions: (editor, session, pos, prefix, callback) ->
@@ -78,7 +107,7 @@ define [
 							else
 								callback null, result
 
-			@editor.completers = [@suggestionManager, SnippetCompleter, ReferencesCompleter]
+			@editor.completers = [@suggestionManager, SnippetCompleter, ReferencesCompleter, LabelsCompleter]
 
 		disable: () ->
 			@editor.setOptions({
@@ -89,18 +118,21 @@ define [
 		onChange: (change) ->
 			cursorPosition = @editor.getCursorPosition()
 			end = change.end
+			range = new Range(end.row, 0, end.row, end.column)
+			lineUpToCursor = @editor.getSession().getTextRange(range)
+			commandFragment = getLastCommandFragment(lineUpToCursor)
 			# Check that this change was made by us, not a collaborator
 			# (Cursor is still one place behind)
-			if end.row == cursorPosition.row and end.column == cursorPosition.column + 1
-				if change.action == "insert"
-					range = new Range(end.row, 0, end.row, end.column)
-					lineUpToCursor = @editor.getSession().getTextRange(range)
-					commandFragment = getLastCommandFragment(lineUpToCursor)
-
-					if commandFragment? and commandFragment.length > 2
-						setTimeout () =>
-							@editor.execCommand("startAutocomplete")
-						, 0
+			# NOTE: this is also the case when a user backspaces over a highlighted region
+			if (
+				change.action == "insert" and
+				end.row == cursorPosition.row and
+				end.column == cursorPosition.column + 1
+			)
+				if commandFragment? and commandFragment.length > 2
+					setTimeout () =>
+						@editor.execCommand("startAutocomplete")
+					, 0
 
 		monkeyPatchAutocomplete: () ->
 			Autocomplete = ace.require("ace/autocomplete").Autocomplete

+ 64 - 0
services/web/public/coffee/ide/editor/directives/aceEditor/labels/LabelsManager.coffee

@@ -0,0 +1,64 @@
+define [
+	"ace/ace"
+], () ->
+	Range = ace.require("ace/range").Range
+
+	getLastCommandFragment = (lineUpToCursor) ->
+		if m = lineUpToCursor.match(/(\\[^\\]+)$/)
+			return m[1]
+		else
+			return null
+
+	class LabelsManager
+		constructor: (@$scope, @editor, @element) ->
+
+			@state =
+				documents: {} # map of DocId => List[Label]
+
+			@loadLabelsTimeout = null
+
+			onChange = (change) =>
+				cursorPosition = @editor.getCursorPosition()
+				end = change.end
+				range = new Range(end.row, 0, end.row, end.column)
+				lineUpToCursor = @editor.getSession().getTextRange(range)
+				commandFragment = getLastCommandFragment(lineUpToCursor)
+				if (
+					change.action in ['remove', 'insert'] and
+					((_.any(change.lines, (line) -> line.match(/\\label\{[^\}\n\\]{0,80}\}/))) or
+					 (commandFragment?.length > 2 and commandFragment.slice(0,7) == '\\label{'))
+				)
+					@scheduleLoadLabelsFromOpenDoc()
+
+			@editor.on "changeSession", (e) =>
+				e.oldSession.off "change", onChange
+				e.session.on "change", onChange
+				setTimeout(
+					() =>
+						@scheduleLoadLabelsFromOpenDoc()
+					, 0
+				)
+
+		loadLabelsFromOpenDoc: () ->
+			docId = @$scope.docId
+			docText = @editor.getValue()
+			labels = []
+			re = /\\label\{([^\}\n\\]{0,80})\}/g
+			while (labelMatch = re.exec(docText)) and labels.length < 1000
+				if labelMatch[1]
+					labels.push(labelMatch[1])
+			@state.documents[docId] = labels
+
+		scheduleLoadLabelsFromOpenDoc: () ->
+			# De-bounce loading labels with a timeout
+			if @loadLabelsTimeout
+				clearTimeout(@loadLabelsTimeout)
+			@loadLabelsTimeout = setTimeout(
+				() =>
+					@loadLabelsFromOpenDoc()
+				, 1000
+				, this
+			)
+
+		getAllLabels: () ->
+			_.flatten(labels for docId, labels of @state.documents)

+ 53 - 0
services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee

@@ -214,6 +214,59 @@ define [
 		rejectChangeIds: (change_ids) ->
 			changes = @rangesTracker.getChanges(change_ids)
 			return if changes.length == 0
+
+			# When doing bulk rejections, adjacent changes might interact with each other.
+			# Consider an insertion with an adjacent deletion (which is a common use-case, replacing words):
+			#
+			#     "foo bar baz" -> "foo quux baz"
+			#
+			# The change above will be modeled with two ops, with the insertion going first:
+			#
+			#     foo quux baz
+			#         |--| -> insertion of "quux", op 1, at position 4
+			#             | -> deletion of "bar", op 2, pushed forward by "quux" to position 8 
+			#
+			# When rejecting these changes at once, if the insertion is rejected first, we get unexpected 
+			# results. What happens is:
+			#
+			#     1) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars 
+			#        starting from position 4;
+			#
+			#           "foo quux baz" -> "foo  baz"
+			#                |--| -> 4 characters to be removed
+			#
+			#     2) Rejecting the deletion adds the deleted word "bar" at position 8 (i.e. it will act as if
+			#        the word "quuux" was still present).
+			#
+			#            "foo  baz" -> "foo  bazbar"
+			#                     | -> deletion of "bar" is reverted by reinserting "bar" at position 8
+			#
+			# While the intended result would be "foo bar baz", what we get is:
+			#
+			#      "foo  bazbar" (note "bar" readded at position 8)
+			#
+			# The issue happens because of step 1. To revert the insertion of "quux", 4 characters are deleted
+			# from position 4. This includes the position where the deletion exists; when that position is 
+			# cleared, the RangesTracker considers that the deletion is gone and stops tracking/updating it.
+			# As we still hold a reference to it, the code tries to revert it by readding the deleted text, but
+			# does so at the outdated position (position 8, which was valid when "quux" was present).
+			#
+			# To avoid this kind of problem, we need to make sure that reverting operations doesn't affect 
+			# subsequent operations that come after. Reverse sorting the operations based on position will 
+			# achieve it; in the case above, it makes sure that the the deletion is reverted first:
+			#
+			#     1) Rejecting the deletion adds the deleted word "bar" at position 8 
+			#
+			#            "foo quux baz" -> "foo quuxbar baz"
+			#                                       | -> deletion of "bar" is reverted by 
+			#                                            reinserting "bar" at position 8
+			#
+			#     2) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars 
+			#        starting from position 4 and achieves the expected result:
+			#
+			#           "foo quuxbar baz" -> "foo bar baz"
+			#                |--| -> 4 characters to be removed
+			
 			changes.sort((a, b) -> b.op.p - a.op.p)
 
 			session = @editor.getSession()

+ 8 - 2
services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee

@@ -19,12 +19,18 @@ define [
 		$scope.finishRenaming = () ->
 			$scope.state.renaming = false
 			newName = $scope.inputs.name
-			if !newName? or newName.length == 0 or newName.length > MAX_PROJECT_NAME_LENGTH
-				return
 			if $scope.project.name == newName
 				return
+			oldName = $scope.project.name
 			$scope.project.name = newName
 			settings.saveProjectSettings({name: $scope.project.name})
+				.error (response, statusCode) ->
+					$scope.project.name = oldName
+					if statusCode == 400
+						ide.showGenericMessageModal("Error renaming project", response)
+					else
+						ide.showGenericMessageModal("Error renaming project", "Please try again in a moment")
+					console.log arguments
 
 		ide.socket.on "projectNameUpdated", (name) ->
 			$scope.$apply () ->

+ 3 - 7
services/web/public/coffee/ide/settings/services/settings.coffee

@@ -12,8 +12,7 @@ define [
 				# End of tracking code.
 				
 				data._csrf = window.csrfToken
-				ide.$http.post "/user/settings", data
-
+				return ide.$http.post "/user/settings", data
 
 			saveProjectSettings: (data) ->
 				# Tracking code.
@@ -24,9 +23,8 @@ define [
 				# End of tracking code.
 				
 				data._csrf = window.csrfToken
-				ide.$http.post "/project/#{ide.project_id}/settings", data
+				return ide.$http.post "/project/#{ide.project_id}/settings", data
 
-				
 			saveProjectAdminSettings: (data) ->
 				# Tracking code.
 				for key in Object.keys(data)
@@ -36,8 +34,6 @@ define [
 				# End of tracking code.
 				
 				data._csrf = window.csrfToken
-				ide.$http.post "/project/#{ide.project_id}/settings/admin", data
-
-				
+				return ide.$http.post "/project/#{ide.project_id}/settings/admin", data
 		}
 	]

+ 41 - 7
services/web/public/coffee/main/project-list/modal-controllers.coffee

@@ -1,9 +1,13 @@
 define [
 	"base"
 ], (App) ->
-	App.controller 'RenameProjectModalController', ($scope, $modalInstance, $timeout, projectName) ->
+	App.controller 'RenameProjectModalController', ($scope, $modalInstance, $timeout, project, queuedHttp) ->
 		$scope.inputs = 
-			projectName: projectName
+			projectName: project.name
+		
+		$scope.state =
+			inflight: false
+			error: false
 
 		$modalInstance.opened.then () ->
 			$timeout () ->
@@ -11,7 +15,20 @@ define [
 			, 200
 
 		$scope.rename = () ->
-			$modalInstance.close($scope.inputs.projectName)
+			$scope.state.inflight = true
+			$scope.state.error = false
+			$scope
+				.renameProject(project, $scope.inputs.projectName)
+				.success () ->
+					$scope.state.inflight = false
+					$scope.state.error = false
+					$modalInstance.close()
+				.error (body, statusCode) ->
+					$scope.state.inflight = false
+					if statusCode == 400
+						$scope.state.error = { message: body }
+					else
+						$scope.state.error = true
 
 		$scope.cancel = () ->
 			$modalInstance.dismiss('cancel')
@@ -21,6 +38,7 @@ define [
 			projectName: project.name + " (Copy)"
 		$scope.state =
 			inflight: false
+			error: false
 
 		$modalInstance.opened.then () ->
 			$timeout () ->
@@ -31,9 +49,16 @@ define [
 			$scope.state.inflight = true
 			$scope
 				.cloneProject(project, $scope.inputs.projectName)
-				.then (project_id) ->
+				.success () ->
 					$scope.state.inflight = false
-					$modalInstance.close(project_id)
+					$scope.state.error = false
+					$modalInstance.close()
+				.error (body, statusCode) ->
+					$scope.state.inflight = false
+					if statusCode == 400
+						$scope.state.error = { message: body }
+					else
+						$scope.state.error = true
 
 		$scope.cancel = () ->
 			$modalInstance.dismiss('cancel')
@@ -43,6 +68,7 @@ define [
 			projectName: ""
 		$scope.state =
 			inflight: false
+			error: false
 
 		$modalInstance.opened.then () ->
 			$timeout () ->
@@ -51,11 +77,19 @@ define [
 
 		$scope.create = () ->
 			$scope.state.inflight = true
+			$scope.state.error = false
 			$scope
 				.createProject($scope.inputs.projectName, template)
-				.then (project_id) ->
+				.success (data) ->
+					$scope.state.inflight = false
+					$scope.state.error = false
+					$modalInstance.close(data.project_id)
+				.error (body, statusCode) ->
 					$scope.state.inflight = false
-					$modalInstance.close(project_id)
+					if statusCode == 400
+						$scope.state.error = { message: body }
+					else
+						$scope.state.error = true
 
 		$scope.cancel = () ->
 			$modalInstance.dismiss('cancel')

+ 7 - 28
services/web/public/coffee/main/project-list/project-list.coffee

@@ -256,9 +256,7 @@ define [
 			)
 
 		$scope.createProject = (name, template = "none") ->
-			deferred = $q.defer()
-
-			queuedHttp
+			return queuedHttp
 				.post("/project/new", {
 					_csrf: window.csrfToken
 					projectName: name
@@ -273,14 +271,8 @@ define [
 						# to the rest of the app
 					}
 					$scope.updateVisibleProjects()
-					deferred.resolve(data.project_id)
-				)
-				.error((data, status, headers, config) ->
-					deferred.reject()
 				)
 
-			return deferred.promise
-
 		$scope.openCreateProjectModal = (template = "none") ->
 			event_tracking.send 'project-list-page-interaction', 'new-project', template
 			modalInstance = $modal.open(
@@ -294,15 +286,13 @@ define [
 			modalInstance.result.then (project_id) ->
 				window.location = "/project/#{project_id}"
 
-		MAX_PROJECT_NAME_LENGTH = 150
 		$scope.renameProject = (project, newName) ->
-			if !newName? or newName.length == 0 or newName.length > MAX_PROJECT_NAME_LENGTH
-				return
-			project.name = newName
-			queuedHttp.post "/project/#{project.id}/rename", {
-				newProjectName: project.name
+			return queuedHttp.post "/project/#{project.id}/rename", {
+				newProjectName: newName,
 				_csrf: window.csrfToken
 			}
+				.success () ->
+					project.name = newName
 
 		$scope.openRenameProjectModal = () ->
 			project = $scope.getFirstSelectedProject()
@@ -312,16 +302,11 @@ define [
 				templateUrl: "renameProjectModalTemplate"
 				controller: "RenameProjectModalController"
 				resolve:
-					projectName: () -> project.name
-			)
-
-			modalInstance.result.then(
-				(newName) ->
-					$scope.renameProject(project, newName)
+					project: () -> project
+				scope: $scope
 			)
 
 		$scope.cloneProject = (project, cloneName) ->
-			deferred = $q.defer()
 			event_tracking.send 'project-list-page-interaction', 'project action', 'Clone'
 			queuedHttp
 				.post("/project/#{project.id}/clone", {
@@ -337,13 +322,7 @@ define [
 						# to the rest of the app
 					}
 					$scope.updateVisibleProjects()
-					deferred.resolve(data.project_id)
 				)
-				.error((data, status, headers, config) ->
-					deferred.reject()
-				)
-
-			return deferred.promise
 
 		$scope.openCloneProjectModal = () ->
 			project = $scope.getFirstSelectedProject()

+ 13 - 8
services/web/public/coffee/services/queued-http.coffee

@@ -19,24 +19,29 @@ define [
 						processPendingRequests()
 
 		queuedHttp = (args...) ->
-			deferred = $q.defer()
-			promise = deferred.promise
+			# We can't use Angular's $q.defer promises, because it only passes
+			# a single argument on error, and $http passes multiple.
+			promise = {}
+			successCallbacks = []
+			errorCallbacks = []
 
 			# Adhere to the $http promise conventions
 			promise.success = (callback) ->
-				promise.then(callback)
+				successCallbacks.push callback
 				return promise
 
 			promise.error = (callback) ->
-				promise.catch(callback)
+				errorCallbacks.push callback
 				return promise
 
 			doRequest = () ->
 				$http(args...)
-					.success (successArgs...) ->
-						deferred.resolve(successArgs...)
-					.error (errorArgs...) ->
-						deferred.reject(errorArgs...)
+					.success (args...) ->
+						for cb in successCallbacks
+							cb(args...)
+					.error (args...) ->
+						for cb in errorCallbacks
+							cb(args...)
 
 			pendingRequests.push doRequest
 			processPendingRequests()

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
services/web/public/js/libs/raven-3.15.0.min.js


+ 4 - 5
services/web/public/stylesheets/app/editor.less

@@ -35,11 +35,8 @@
 }
 
 .global-alerts {
-	position: absolute;
-	z-index: 20;
-	top: 2px;
-	left: 0;
-	right: 0;
+	height: 0;
+	margin-top: 2px;
 	text-align: center;
 
 	.alert {
@@ -49,6 +46,8 @@
 		padding: (@line-height-computed / 4);
 		font-size: 14px;
 		margin-bottom: (@line-height-computed / 4);
+		position: relative;
+		z-index: 20;
 	}
 }
 	#try-reconnect-now-button {

+ 1 - 1
services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee

@@ -547,7 +547,7 @@ describe "EditorController", ->
 			@err = "errro"
 			@window_id = "kdsjklj290jlk"
 			@newName = "new name here"
-			@ProjectDetailsHandler.renameProject = sinon.stub().callsArgWith(2, @err)
+			@ProjectDetailsHandler.renameProject = sinon.stub().callsArg(2)
 			@EditorRealTimeController.emitToRoom = sinon.stub()
 
 		it "should call the EditorController", (done)->

+ 5 - 13
services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee

@@ -280,20 +280,12 @@ describe "ProjectController", ->
 				done()
 			@ProjectController.renameProject @req, @res
 
-		it "should send a 500 if there is a problem", (done)->
-			@EditorController.renameProject.callsArgWith(2, "problem")
-			@res.sendStatus = (code)=>
-				code.should.equal 500
-				@EditorController.renameProject.calledWith(@project_id, @newProjectName).should.equal true
+		it "should send an error to next() if there is a problem", (done)->
+			@EditorController.renameProject.callsArgWith(2, error = new Error("problem"))
+			next = (e)=>
+				e.should.equal error
 				done()
-			@ProjectController.renameProject @req, @res
-
-		it "should return an error if the name is over 150 chars", (done)->
-			@req.body.newProjectName = "EDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOTEDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOT"
-			@res.sendStatus = (code)=>
-				code.should.equal 400
-				done()
-			@ProjectController.renameProject @req, @res
+			@ProjectController.renameProject @req, @res, next
 
 	describe "loadEditor", ->
 		beforeEach ->

+ 15 - 0
services/web/test/UnitTests/coffee/Project/ProjectCreationHandlerTests.coffee

@@ -34,6 +34,8 @@ describe 'ProjectCreationHandler', ->
 			addDoc: sinon.stub().callsArgWith(4, null, {_id: docId})
 			addFile: sinon.stub().callsArg(4)
 			setRootDoc: sinon.stub().callsArg(2)
+		@ProjectDetailsHandler =
+			validateProjectName: sinon.stub().yields()
 
 		@user = 
 			first_name:"first name here"
@@ -48,6 +50,7 @@ describe 'ProjectCreationHandler', ->
 			'../../models/Project':{Project:@ProjectModel}
 			'../../models/Folder':{Folder:@FolderModel}
 			'./ProjectEntityHandler':@ProjectEntityHandler
+			"./ProjectDetailsHandler":@ProjectDetailsHandler
 			"settings-sharelatex": @Settings = {}
 			'logger-sharelatex': {log:->}
 			"metrics-sharelatex": {
@@ -96,6 +99,18 @@ describe 'ProjectCreationHandler', ->
 			
 			it 'should return the error to the callback', ->
 				should.exist @callback.args[0][0]
+		
+		describe "with an invalid name", ->
+			beforeEach ->
+				@ProjectDetailsHandler.validateProjectName = sinon.stub().yields(new Error("bad name"))
+				@handler.createBlankProject ownerId, projectName, @callback
+			
+			it 'should return the error to the callback', ->
+				should.exist @callback.args[0][0]
+			
+			it 'should not try to create the project', ->
+				@ProjectModel::save.called.should.equal false
+				
 
 	describe 'Creating a basic project', ->
 		beforeEach ->

+ 29 - 0
services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee

@@ -4,6 +4,7 @@ Errors = require "../../../../app/js/Features/Errors/Errors"
 SandboxedModule = require('sandboxed-module')
 sinon = require('sinon')
 assert = require("chai").assert
+expect = require("chai").expect
 require('chai').should()
 
 describe 'ProjectDetailsHandler', ->
@@ -95,6 +96,7 @@ describe 'ProjectDetailsHandler', ->
 
 	describe "renameProject", ->
 		beforeEach ->
+			@handler.validateProjectName = sinon.stub().yields()
 			@ProjectModel.update.callsArgWith(2)
 			@newName = "new name here"
 
@@ -108,7 +110,34 @@ describe 'ProjectDetailsHandler', ->
 			@handler.renameProject @project_id, @newName, =>
 				@tpdsUpdateSender.moveEntity.calledWith({project_id:@project_id, project_name:@project.name, newProjectName:@newName}).should.equal true
 				done()
+		
+		it "should not do anything with an invalid name", (done) ->
+			@handler.validateProjectName = sinon.stub().yields(new Error("invalid name"))
+			@handler.renameProject @project_id, @newName, =>
+				@tpdsUpdateSender.moveEntity.called.should.equal false
+				@ProjectModel.update.called.should.equal false
+				done()
 
+	describe "validateProjectName", ->
+		it "should reject empty names", (done) ->
+			@handler.validateProjectName "", (error) ->
+				expect(error).to.exist
+				done()
+
+		it "should reject empty names with /s", (done) ->
+			@handler.validateProjectName "foo/bar", (error) ->
+				expect(error).to.exist
+				done()
+
+		it "should reject long names", (done) ->
+			@handler.validateProjectName new Array(1000).join("a"), (error) ->
+				expect(error).to.exist
+				done()
+
+		it "should accept normal names", (done) ->
+			@handler.validateProjectName "foobar", (error) ->
+				expect(error).to.not.exist
+				done()
 
 	describe "setPublicAccessLevel", ->
 		beforeEach ->

+ 1 - 0
services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee

@@ -80,6 +80,7 @@ describe 'ProjectEntityHandler', ->
 			'./ProjectUpdateHandler': @projectUpdater
 			"./ProjectGetter": @ProjectGetter
 			"settings-sharelatex":@settings
+			"../Cooldown/CooldownManager": @CooldownManager = {}
 
 
 	describe 'mkdirp', ->

+ 0 - 1
services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee

@@ -70,7 +70,6 @@ describe 'ProjectLocator', ->
 
 		it 'should give error if element could not be found', (done)->
 			@locator.findElement {project_id:project._id, element_id:"ddsd432nj42", type:"docs"}, (err, foundElement, path, parentFolder)->
-				console.log err
 				err.should.deep.equal new Errors.NotFoundError("entity not found")
 				done()
 

+ 1 - 0
services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee

@@ -11,6 +11,7 @@ describe 'ProjectUpdateHandler', ->
 		@ProjectModel.update = sinon.stub().callsArg(3)
 		@handler = SandboxedModule.require modulePath, requires:
 			'../../models/Project':{Project:@ProjectModel}
+			'logger-sharelatex' : { log: sinon.stub() }
 
 	describe 'marking a project as recently updated', ->
 		it 'should send an update to mongo', (done)->

+ 1 - 0
services/web/test/UnitTests/coffee/User/UserLocatorTests.coffee

@@ -11,6 +11,7 @@ describe "UserLocator", ->
 		@UserLocator = SandboxedModule.require modulePath, requires:
 			"../../infrastructure/mongojs": db: @db =  { users: {} }
 			"metrics-sharelatex": timeAsyncMethod: sinon.stub()
+			'logger-sharelatex' : { log: sinon.stub() }
 		@db.users =
 			findOne : sinon.stub().callsArgWith(1, null, @user)
 

+ 0 - 1
services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee

@@ -96,7 +96,6 @@ describe "UserRegistrationHandler", ->
 			it "should return email registered in the error if there is a non holdingAccount there", (done)->
 				@User.findOne.callsArgWith(1, null, @user = {holdingAccount:false})
 				@handler.registerNewUser @passingRequest, (err, user)=>
-					console.log err, user
 					err.should.deep.equal new Error("EmailAlreadyRegistered")
 					user.should.deep.equal @user
 					done()

Некоторые файлы не были показаны из-за большого количества измененных файлов