Ver Fonte

Merge branch 'master' into pr-style-v2-resizers

Paulo Reis há 9 anos atrás
pai
commit
7c5a7880b0
49 ficheiros alterados com 916 adições e 313 exclusões
  1. 19 8
      services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee
  2. 10 10
      services/web/app/coffee/Features/Editor/EditorController.coffee
  3. 3 2
      services/web/app/coffee/Features/Editor/EditorHttpController.coffee
  4. 6 5
      services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee
  5. 12 2
      services/web/app/coffee/Features/Project/ProjectController.coffee
  6. 4 4
      services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
  7. 9 9
      services/web/app/coffee/Features/Project/ProjectDuplicator.coffee
  8. 61 50
      services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
  9. 5 5
      services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee
  10. 5 5
      services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee
  11. 5 1
      services/web/app/views/project/editor/editor.pug
  12. 1 1
      services/web/app/views/project/editor/header.pug
  13. 56 0
      services/web/app/views/project/list/modals.pug
  14. 2 2
      services/web/app/views/project/list/project-list.pug
  15. 4 5
      services/web/app/views/project/list/v1-item.pug
  16. 1 1
      services/web/config/settings.defaults.coffee
  17. 1 0
      services/web/docker-compose.yml
  18. 2 1
      services/web/docker-shared.template.yml
  19. 10 0
      services/web/public/coffee/main/project-list/modal-controllers.coffee
  20. 10 0
      services/web/public/coffee/main/project-list/project-list.coffee
  21. 1 18
      services/web/public/img/ol-brand/overleaf-o-grey.svg
  22. 1 18
      services/web/public/img/ol-brand/overleaf-o-white.svg
  23. 1 10
      services/web/public/img/ol-brand/overleaf-o.svg
  24. BIN
      services/web/public/img/v1-import/v2-editor.png
  25. 1 1
      services/web/public/stylesheets/app/editor/file-tree.less
  26. 5 2
      services/web/public/stylesheets/app/editor/toolbar.less
  27. 24 0
      services/web/public/stylesheets/app/list/v1-import-modal.less
  28. 6 0
      services/web/public/stylesheets/app/project-list.less
  29. 1 0
      services/web/public/stylesheets/core/_common-variables.less
  30. 307 0
      services/web/test/acceptance/coffee/ProjectStructureTests.coffee
  31. 27 0
      services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
  32. 1 0
      services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
  33. 20 0
      services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee
  34. 18 0
      services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee
  35. 6 3
      services/web/test/acceptance/coffee/helpers/User.coffee
  36. BIN
      services/web/test/acceptance/files/1pixel.png
  37. BIN
      services/web/test/acceptance/files/2pixel.png
  38. 1 0
      services/web/test/acceptance/files/test.tex
  39. BIN
      services/web/test/acceptance/files/test_project.zip
  40. 1 1
      services/web/test/acceptance/scripts/full-test.sh
  41. 80 28
      services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
  42. 31 34
      services/web/test/unit/coffee/Editor/EditorControllerTests.coffee
  43. 2 2
      services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
  44. 16 10
      services/web/test/unit/coffee/FileStore/FileStoreHandlerTests.coffee
  45. 7 6
      services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
  46. 22 10
      services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
  47. 90 38
      services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
  48. 9 9
      services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee
  49. 12 12
      services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee

+ 19 - 8
services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee

@@ -204,11 +204,11 @@ module.exports = DocumentUpdaterHandler =
 				logger.error {project_id, doc_id, thread_id}, "doc updater returned a non-success status code: #{res.statusCode}"
 				callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
 
-	updateProjectStructure : (project_id, userId, oldDocs, newDocs, oldFiles, newFiles, callback = (error) ->)->
+	updateProjectStructure : (project_id, userId, changes, callback = (error) ->)->
 		return callback() if !settings.apis.project_history?.enabled
 
-		docUpdates = DocumentUpdaterHandler._getRenameUpdates('doc', oldDocs, newDocs)
-		fileUpdates = DocumentUpdaterHandler._getRenameUpdates('file', oldFiles, newFiles)
+		docUpdates = DocumentUpdaterHandler._getRenameUpdates('doc', changes.oldDocs, changes.newDocs)
+		fileUpdates = DocumentUpdaterHandler._getRenameUpdates('file', changes.oldFiles, changes.newFiles)
 
 		timer = new metrics.Timer("set-document")
 		url = "#{settings.apis.documentupdater.url}/project/#{project_id}"
@@ -231,14 +231,25 @@ module.exports = DocumentUpdaterHandler =
 				callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
 
 	_getRenameUpdates: (entityType, oldEntities, newEntities) ->
+		oldEntities ||= []
+		newEntities ||= []
 		updates = []
 
-		for oldEntity in oldEntities
-			id = oldEntity[entityType]._id
-			newEntity = _.find newEntities, (newEntity) ->
-				newEntity[entityType]._id.toString() == id.toString()
+		oldEntitiesHash = _.indexBy oldEntities, (entity) -> entity[entityType]._id.toString()
+		newEntitiesHash = _.indexBy newEntities, (entity) -> entity[entityType]._id.toString()
 
-			if newEntity.path != oldEntity.path
+		for id, newEntity of newEntitiesHash
+			oldEntity = oldEntitiesHash[id]
+
+			if !oldEntity?
+				# entity added
+				updates.push
+					id: id
+					pathname: newEntity.path
+					docLines: newEntity.docLines
+					url: newEntity.url
+			else if newEntity.path != oldEntity.path
+				# entity renamed
 				updates.push
 					id: id
 					pathname: oldEntity.path

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

@@ -19,48 +19,48 @@ module.exports = EditorController =
 			DocumentUpdaterHandler.flushDocToMongo project_id, doc_id, callback
 
 
-	addDoc: (project_id, folder_id, docName, docLines, source, callback = (error, doc)->)->
+	addDoc: (project_id, folder_id, docName, docLines, source, user_id, callback = (error, doc)->)->
 		LockManager.getLock project_id, (err)->
 			if err?
 				logger.err err:err, project_id:project_id, source:source,  "could not get lock to addDoc"
 				return callback(err)
-			EditorController.addDocWithoutLock project_id, folder_id, docName, docLines, source, (error, doc)->
+			EditorController.addDocWithoutLock project_id, folder_id, docName, docLines, source, user_id, (error, doc)->
 				LockManager.releaseLock project_id, ->
 					callback(error, doc)
 
-	addDocWithoutLock: (project_id, folder_id, docName, docLines, source, callback = (error, doc)->)->
+	addDocWithoutLock: (project_id, folder_id, docName, docLines, source, user_id, callback = (error, doc)->)->
 		docName = docName.trim()
 		logger.log {project_id, folder_id, docName, source}, "sending new doc to project"
 		Metrics.inc "editor.add-doc"
-		ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, (err, doc, folder_id)=>
+		ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, user_id, (err, doc, folder_id)=>
 			if err?
 				logger.err err:err, project_id:project_id, docName:docName, "error adding doc without lock"
 				return callback(err)
 			EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc, source)
 			callback(err, doc)
 
-	addFile: (project_id, folder_id, fileName, path, source, callback = (error, file)->)->
+	addFile: (project_id, folder_id, fileName, path, source, user_id, callback = (error, file)->)->
 		LockManager.getLock project_id, (err)->
 			if err?
 				logger.err err:err, project_id:project_id, source:source,  "could not get lock to addFile"
 				return callback(err)
-			EditorController.addFileWithoutLock project_id, folder_id, fileName, path, source, (error, file)->
+			EditorController.addFileWithoutLock project_id, folder_id, fileName, path, source, user_id, (error, file)->
 				LockManager.releaseLock project_id, ->
 					callback(error, file)	
 
-	addFileWithoutLock: (project_id, folder_id, fileName, path, source, callback = (error, file)->)->
+	addFileWithoutLock: (project_id, folder_id, fileName, path, source, user_id, callback = (error, file)->)->
 		fileName = fileName.trim()
 		logger.log {project_id, folder_id, fileName, path}, "sending new file to project"
 		Metrics.inc "editor.add-file"
-		ProjectEntityHandler.addFile project_id, folder_id, fileName, path, (err, fileRef, folder_id)=>
+		ProjectEntityHandler.addFile project_id, folder_id, fileName, path, user_id, (err, fileRef, folder_id)=>
 			if err?
 				logger.err err:err, project_id:project_id, folder_id:folder_id, fileName:fileName, "error adding file without lock"
 				return callback(err)
 			EditorRealTimeController.emitToRoom(project_id, 'reciveNewFile', folder_id, fileRef, source)
 			callback(err, fileRef)
 
-	replaceFile: (project_id, file_id, fsPath, source, callback = (error) ->)->
-		ProjectEntityHandler.replaceFile project_id, file_id, fsPath, callback
+	replaceFile: (project_id, file_id, fsPath, source, user_id, callback = (error) ->)->
+		ProjectEntityHandler.replaceFile project_id, file_id, fsPath, user_id, callback
 
 	addFolder : (project_id, folder_id, folderName, source, callback = (error, folder)->)->
 		LockManager.getLock project_id, (err)->

+ 3 - 2
services/web/app/coffee/Features/Editor/EditorHttpController.coffee

@@ -81,10 +81,11 @@ module.exports = EditorHttpController =
 		project_id = req.params.Project_id
 		name = req.body.name
 		parent_folder_id = req.body.parent_folder_id
+		user_id = AuthenticationController.getLoggedInUserId(req)
 		logger.log project_id:project_id, name:name, parent_folder_id:parent_folder_id, "getting request to add doc to project"
 		if !EditorHttpController._nameIsAcceptableLength(name)
 			return res.sendStatus 400
-		EditorController.addDoc project_id, parent_folder_id, name, [], "editor", (error, doc) ->
+		EditorController.addDoc project_id, parent_folder_id, name, [], "editor", user_id, (error, doc) ->
 			if error == "project_has_to_many_files"
 				res.status(400).json(req.i18n.translate("project_has_to_many_files"))
 			else if error?
@@ -113,9 +114,9 @@ module.exports = EditorHttpController =
 		entity_id   = req.params.entity_id
 		entity_type = req.params.entity_type
 		name = req.body.name
-		user_id = AuthenticationController.getLoggedInUserId(req)
 		if !EditorHttpController._nameIsAcceptableLength(name)
 			return res.sendStatus 400
+		user_id = AuthenticationController.getLoggedInUserId(req)
 		EditorController.renameEntity project_id, entity_id, entity_type, name, user_id, (error) ->
 			return next(error) if error?
 			res.sendStatus 204

+ 6 - 5
services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee

@@ -21,9 +21,9 @@ module.exports = FileStoreHandler =
 				return callback(new Error("can not upload symlink"))
 
 			_cb = callback
-			callback = (err) ->
+			callback = (err, url) ->
 				callback = ->	# avoid double callbacks
-				_cb(err)
+				_cb(err, url)
 
 			logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk"
 			readStream = fs.createReadStream(fsPath)
@@ -31,9 +31,10 @@ module.exports = FileStoreHandler =
 				logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk"
 				callback err
 			readStream.on "open", () ->
+				url = FileStoreHandler._buildUrl(project_id, file_id)
 				opts =
 					method: "post"
-					uri: FileStoreHandler._buildUrl(project_id, file_id)
+					uri: url
 					timeout:fiveMinsInMs
 				writeStream = request(opts)
 				writeStream.on "error", (err)->
@@ -45,7 +46,7 @@ module.exports = FileStoreHandler =
 						logger.err {err, statusCode: response.statusCode}, "error uploading to filestore"
 						callback(err)
 					else
-						callback(null)
+						callback(null, url)
 				readStream.pipe writeStream
 
 	getFileStream: (project_id, file_id, query, callback)->
@@ -91,7 +92,7 @@ module.exports = FileStoreHandler =
 		request opts, (err)->
 			if err?
 				logger.err err:err, oldProject_id:oldProject_id, oldFile_id:oldFile_id, newProject_id:newProject_id, newFile_id:newFile_id, "something went wrong telling filestore api to copy file"
-			callback(err)
+			callback(err, opts.uri)
 
 	_buildUrl: (project_id, file_id)->
 		return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}"

+ 12 - 2
services/web/app/coffee/Features/Project/ProjectController.coffee

@@ -170,6 +170,8 @@ module.exports = ProjectController =
 					return notification
 				projects = ProjectController._buildProjectList results.projects, results.v1Projects?.projects
 				user = results.user
+				warnings = ProjectController._buildWarningsList results.v1Projects
+
 				ProjectController._injectProjectOwners projects, (error, projects) ->
 					return next(error) if error?
 					viewModel = {
@@ -181,7 +183,7 @@ module.exports = ProjectController =
 						user: user
 						hasSubscription: results.hasSubscription[0]
 						isShowingV1Projects: results.v1Projects?
-						noV1Connection: results.v1Projects?.noConnection
+						warnings: warnings
 					}
 
 					if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key?
@@ -251,7 +253,7 @@ module.exports = ProjectController =
 				# Extract data from user's ObjectId
 				timestamp = parseInt(user_id.toString().substring(0, 8), 16)
 
-				rolloutPercentage = 20 # Percentage of users to roll out to
+				rolloutPercentage = 40 # Percentage of users to roll out to
 				if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage)
 					# Don't show if user is not part of roll out
 					return cb(null, { enabled: false, showOnboarding: false })
@@ -427,6 +429,14 @@ module.exports = ProjectController =
 					project.owner = users[project.owner_ref.toString()]
 			callback null, projects
 
+	_buildWarningsList: (v1ProjectData = {}) ->
+		warnings = []
+		if v1ProjectData.noConnection
+			warnings.push 'No V1 Connection'
+		if v1ProjectData.hasHiddenV1Projects
+			warnings.push "Looks like you've got a lot of V1 projects! Some of them may be hidden on V2. To view them all, use the V1 dashboard."
+		return warnings
+
 defaultSettingsForAnonymousUser = (user_id)->
 	id : user_id
 	ace:

+ 4 - 4
services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee

@@ -52,7 +52,7 @@ module.exports = ProjectCreationHandler =
 			return callback(error) if error?
 			self._buildTemplate "mainbasic.tex", owner_id, projectName, (error, docLines)->
 				return callback(error) if error?
-				ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, (error, doc)->
+				ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, owner_id, (error, doc)->
 					if error?
 						logger.err err:error, "error adding doc when creating basic project"
 						return callback(error)
@@ -67,17 +67,17 @@ module.exports = ProjectCreationHandler =
 				(callback) ->
 					self._buildTemplate "main.tex", owner_id, projectName, (error, docLines)->
 						return callback(error) if error?
-						ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, (error, doc)->
+						ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, owner_id, (error, doc)->
 							return callback(error) if error?
 							ProjectEntityHandler.setRootDoc project._id, doc._id, callback
 				(callback) ->
 					self._buildTemplate "references.bib", owner_id, projectName, (error, docLines)->
 						return callback(error) if error?
-						ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "references.bib", docLines, (error, doc)->
+						ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "references.bib", docLines, owner_id, (error, doc)->
 							callback(error)
 				(callback) ->
 					universePath = Path.resolve(__dirname + "/../../../templates/project_files/universe.jpg")
-					ProjectEntityHandler.addFile project._id, project.rootFolder[0]._id, "universe.jpg", universePath, callback
+					ProjectEntityHandler.addFile project._id, project.rootFolder[0]._id, "universe.jpg", universePath, owner_id, callback
 			], (error) ->
 				callback(error, project)
 

+ 9 - 9
services/web/app/coffee/Features/Project/ProjectDuplicator.coffee

@@ -12,7 +12,7 @@ logger = require("logger-sharelatex")
 
 module.exports = ProjectDuplicator =
 
-	_copyDocs: (newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)->
+	_copyDocs: (owner_id, newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)->
 		setRootDoc = _.once (doc_id)->
 			projectEntityHandler.setRootDoc newProject._id, doc_id
 		docs = originalFolder.docs or []
@@ -21,7 +21,7 @@ module.exports = ProjectDuplicator =
 				if !doc?._id?
 					return callback()
 				content = docContents[doc._id.toString()]
-				projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, (err, newDoc)->
+				projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)->
 					if err?
 						logger.err err:err, "error copying doc"
 						return callback(err)
@@ -31,15 +31,15 @@ module.exports = ProjectDuplicator =
 
 		async.series jobs, callback
 
-	_copyFiles: (newProject, originalProject_id, originalFolder, desFolder, callback)->
+	_copyFiles: (owner_id, newProject, originalProject_id, originalFolder, desFolder, callback)->
 		fileRefs = originalFolder.fileRefs or []
 		jobs = fileRefs.map (file)->
 			return (cb)->
-				projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, cb
+				projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, owner_id, cb
 		async.parallelLimit jobs, 5, callback
 
 
-	_copyFolderRecursivly: (newProject_id, originalProject_id, originalRootDoc, originalFolder, desFolder, docContents, callback)->
+	_copyFolderRecursivly: (owner_id, newProject_id, originalProject_id, originalRootDoc, originalFolder, desFolder, docContents, callback)->
 		ProjectGetter.getProject newProject_id, {rootFolder:true, name:true}, (err, newProject)->
 			if err?
 				logger.err project_id:newProject_id, "could not get project"
@@ -53,12 +53,12 @@ module.exports = ProjectDuplicator =
 						return cb()
 					projectEntityHandler.addFolderWithProject newProject, desFolder?._id, childFolder.name, (err, newFolder)->
 						return cb(err) if err?
-						ProjectDuplicator._copyFolderRecursivly newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb
+						ProjectDuplicator._copyFolderRecursivly owner_id, newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb
 
 			jobs.push (cb)->
-				ProjectDuplicator._copyFiles newProject, originalProject_id, originalFolder, desFolder, cb
+				ProjectDuplicator._copyFiles owner_id, newProject, originalProject_id, originalFolder, desFolder, cb
 			jobs.push (cb)->
-				ProjectDuplicator._copyDocs newProject, originalRootDoc, originalFolder, desFolder, docContents, cb
+				ProjectDuplicator._copyDocs owner_id, newProject, originalRootDoc, originalFolder, desFolder, docContents, cb
 
 			async.series jobs, callback
 
@@ -90,7 +90,7 @@ module.exports = ProjectDuplicator =
 			
 			projectOptionsHandler.setCompiler newProject._id, originalProject.compiler, ->
 
-			ProjectDuplicator._copyFolderRecursivly newProject._id, originalProject_id, originalRootDoc, originalProject.rootFolder[0], newProject.rootFolder[0], docContents, ->
+			ProjectDuplicator._copyFolderRecursivly owner._id, newProject._id, originalProject_id, originalRootDoc, originalProject.rootFolder[0], newProject.rootFolder[0], docContents, ->
 				if err?
 					logger.err err:err, originalProject_id:originalProject_id,  newProjectName:newProjectName, "error cloning project"
 				callback(err, newProject)

+ 61 - 50
services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee

@@ -16,7 +16,7 @@ projectUpdateHandler = require('./ProjectUpdateHandler')
 DocstoreManager = require "../Docstore/DocstoreManager"
 ProjectGetter = require "./ProjectGetter"
 CooldownManager = require '../Cooldown/CooldownManager'
-
+DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
 
 module.exports = ProjectEntityHandler =
 	getAllFolders: (project_id,  callback) ->
@@ -106,8 +106,7 @@ module.exports = ProjectEntityHandler =
 	flushProjectToThirdPartyDataStore: (project_id, callback) ->
 		self = @
 		logger.log project_id:project_id, "flushing project to tpds"
-		documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
-		documentUpdaterHandler.flushProjectToMongo project_id, (error) ->
+		DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
 			return callback(error) if error?
 			ProjectGetter.getProject project_id, {name:true}, (error, project) ->
 				return callback(error) if error?
@@ -150,14 +149,14 @@ module.exports = ProjectEntityHandler =
 		else
 			DocstoreManager.getDoc project_id, doc_id, options, callback
 
-	addDoc: (project_id, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=>
+	addDoc: (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
 		ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
 			if err?
 				logger.err project_id:project_id, err:err, "error getting project for add doc"
 				return callback(err)
-			ProjectEntityHandler.addDocWithProject project, folder_id, docName, docLines, callback
+			ProjectEntityHandler.addDocWithProject project, folder_id, docName, docLines, userId, callback
 
-	addDocWithProject: (project, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=>
+	addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
 		project_id = project._id
 		logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project"
 		confirmFolder project, folder_id, (folder_id)=>
@@ -177,7 +176,14 @@ module.exports = ProjectEntityHandler =
 						rev:          0
 					}, (err) ->
 						return callback(err) if err?
-						callback(null, doc, folder_id)
+						newDocs = [
+							doc: doc
+							path: result?.path?.fileSystem
+							docLines: docLines.join('\n')
+						]
+						DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) ->
+							return callback(error) if error?
+							callback null, doc, folder_id
 
 	restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
 		# getDoc will return the deleted doc's lines, but we don't actually remove
@@ -186,20 +192,20 @@ module.exports = ProjectEntityHandler =
 			return callback(error) if error?
 			ProjectEntityHandler.addDoc project_id, null, name, lines, callback
 
-	addFile: (project_id, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)->
+	addFile: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)->
 		ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
 			if err?
 				logger.err project_id:project_id, err:err, "error getting project for add file"
 				return callback(err)
-			ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, callback
+			ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, userId, callback
 
-	addFileWithProject: (project, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)->
+	addFileWithProject: (project, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)->
 		project_id = project._id
 		logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
 		return callback(err) if err?
 		confirmFolder project, folder_id, (folder_id)->
 			fileRef = new File name : fileName
-			FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err)->
+			FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err, fileStoreUrl)->
 				if err?
 					logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
 					return callback(err)
@@ -209,27 +215,31 @@ module.exports = ProjectEntityHandler =
 						return callback(err)
 					tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) ->
 						return callback(err) if err?
-						callback(null, fileRef, folder_id)
+						newFiles = [
+							file: fileRef
+							path: result?.path?.fileSystem
+							url: fileStoreUrl
+						]
+						DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
+							return callback(error) if error?
+							callback null, fileRef, folder_id
 
-	replaceFile: (project_id, file_id, fsPath, callback)->
-		ProjectGetter.getProject project_id, {name:true}, (err, project) ->
+	replaceFile: (project_id, file_id, fsPath, userId, callback)->
+		self = ProjectEntityHandler
+		FileStoreHandler.uploadFileFromDisk project_id, file_id, fsPath, (err, fileStoreUrl)->
 			return callback(err) if err?
-			findOpts =
-				project_id:project._id
-				element_id:file_id
-				type:"file"
-			FileStoreHandler.uploadFileFromDisk project._id, file_id, fsPath, (err)->
+			ProjectGetter.getProject project_id, {rootFolder: true, name:true}, (err, project) ->
 				return callback(err) if err?
 				# Note there is a potential race condition here (and elsewhere)
 				# If the file tree changes between findElement and the Project.update
 				# then the path to the file element will be out of date. In practice
 				# this is not a problem so long as we do not do anything longer running
 				# between them (like waiting for the file to upload.)
-				projectLocator.findElement findOpts, (err, fileRef, path)=>
+				projectLocator.findElement {project:project, element_id: file_id, type: 'file'}, (err, fileRef, path)=>
 					return callback(err) if err?
-					tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, (error) ->
+					tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, (err) ->
 						return callback(err) if err?
-						conditons = _id:project._id
+						conditions = _id:project._id
 						inc = {}
 						inc["#{path.mongo}.rev"] = 1
 						set = {}
@@ -237,39 +247,43 @@ module.exports = ProjectEntityHandler =
 						update =
 							"$inc": inc
 							"$set": set
-						Project.update conditons, update, {}, (err, second)->
-							callback()
-
-	copyFileFromExistingProject: (project_id, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)->
-		logger.log project_id:project_id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3"
-		ProjectGetter.getProject project_id, {name:true}, (err, project) ->
-			if err?
-				logger.err project_id:project_id, err:err, "error getting project for copy file from existing project"
-				return callback(err)
-			ProjectEntityHandler.copyFileFromExistingProjectWithProject project, folder_id, originalProject_id, origonalFileRef, callback
-
-
-	copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)->
+						Project.findOneAndUpdate conditions, update, { "new": true}, (err) ->
+							return callback(err) if err?
+							newFiles = [
+								file: fileRef
+								path: path.fileSystem
+								url: fileStoreUrl
+							]
+							DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, callback
+
+	copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
 		project_id = project._id
-		logger.log project_id:project_id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3 with project"
+		logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
 		return callback(err) if err?
 		confirmFolder project, folder_id, (folder_id)=>
 			if !origonalFileRef?
-				logger.err project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "file trying to copy is null"
+				logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
 				return callback()
 			fileRef = new File name : origonalFileRef.name
-			FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err)->
+			FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
 				if err?
-					logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error coping file in s3"
+					logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
 					return callback(err)
 				ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
 					if err?
-						logger.err err:err, project_id:project._id, folder_id:folder_id, "error putting element as part of copy"
+						logger.err { err, project_id, folder_id }, "error putting element as part of copy"
 						return callback(err)
-					tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
+					tpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
 						if err?
-							logger.err err:err,  project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error sending file to tpds worker"
-						callback(null, fileRef, folder_id)
+							logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
+						newFiles = [
+							file: fileRef
+							path: result?.path?.fileSystem
+							url: fileStoreUrl
+						]
+						DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
+							return callback(error) if error?
+							callback null, fileRef, folder_id
 
 	mkdirp: (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
 		self = @
@@ -381,11 +395,9 @@ module.exports = ProjectEntityHandler =
 									endPath: result.path.fileSystem,
 									rev: entity.rev
 								tpdsUpdateSender.moveEntity opts
-								self.getAllEntitiesFromProject newProject, (error, newDocs, newFiles
-								) =>
+								self.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) =>
 									return callback(error) if error?
-									documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
-									documentUpdaterHandler.updateProjectStructure project_id, userId, oldDocs, newDocs, oldFiles, newFiles, callback
+									DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback
 
 	_checkValidMove: (project, entityType, entityPath, destFolderId, callback = (error) ->) ->
 		return callback() if !entityType.match(/folder/)
@@ -442,8 +454,7 @@ module.exports = ProjectEntityHandler =
 						return callback(error) if error?
 						ProjectEntityHandler.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) =>
 							return callback(error) if error?
-							documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
-							documentUpdaterHandler.updateProjectStructure project_id, userId, oldDocs, newDocs, oldFiles, newFiles, callback
+							DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback
 
 	_cleanUpEntity: (project, entity, entityType, callback = (error) ->) ->
 		if(entityType.indexOf("file") != -1)
@@ -466,7 +477,7 @@ module.exports = ProjectEntityHandler =
 
 		unsetRootDocIfRequired (error) ->
 			return callback(error) if error?
-			require('../../Features/DocumentUpdater/DocumentUpdaterHandler').deleteDoc project_id, doc_id, (error) ->
+			DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) ->
 				return callback(error) if error?
 				ProjectEntityHandler._insertDeletedDocReference project._id, doc, (error) ->
 					return callback(error) if error?

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

@@ -28,7 +28,7 @@ module.exports =
 				FileTypeManager.isBinary path, fsPath, (err, isFile)->
 					return callback(err) if err?
 					if isFile
-						self.p.processFile project_id, elementId, fsPath, path, source, callback
+						self.p.processFile project_id, elementId, fsPath, path, source, user_id, callback
 					else
 						self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback
 
@@ -57,9 +57,9 @@ module.exports =
 						if err?
 							logger.err err:err, project_id:project_id, doc_id:doc_id, path:path, "error processing file"
 							return callback(err)
-						editorController.addDoc project_id, folder._id, fileName, docLines, source, callback
+						editorController.addDoc project_id, folder._id, fileName, docLines, source, user_id, callback
 
-		processFile: (project_id, file_id, fsPath, path, source, callback)->
+		processFile: (project_id, file_id, fsPath, path, source, user_id, callback)->
 			finish = (err)->
 				logger.log project_id:project_id, file_id:file_id, path:path, "completed processing file update from tpds"
 				callback(err)
@@ -69,9 +69,9 @@ module.exports =
 					logger.err err:err, project_id:project_id, file_id:file_id, path:path, "error processing file"
 					return callback(err)
 				else if file_id?
-					editorController.replaceFile project_id, file_id, fsPath, source, finish
+					editorController.replaceFile project_id, file_id, fsPath, source, user_id, finish
 				else
-					editorController.addFile project_id, folder?._id, fileName, fsPath, source, finish
+					editorController.addFile project_id, folder?._id, fileName, fsPath, source, user_id, finish
 
 		writeStreamToDisk: (project_id, file_id, stream, callback = (err, fsPath)->)->
 			if !file_id?

+ 5 - 5
services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee

@@ -28,9 +28,9 @@ module.exports = FileSystemImportManager =
 						if existingDoc?
 							EditorController.setDoc project_id, existingDoc._id, user_id, lines, "upload", callback
 						else
-							EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
+							EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", user_id, callback
 				else
-					EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
+					EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", user_id, callback
 
 	addFile: (user_id, project_id, folder_id, name, path, replace, callback = (error, file)-> )->
 		FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
@@ -39,7 +39,7 @@ module.exports = FileSystemImportManager =
 				return callback("path is symlink")
 
 			if !replace
-				EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
+				EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", user_id, callback
 			else
 				ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
 					return callback(error) if error?
@@ -50,9 +50,9 @@ module.exports = FileSystemImportManager =
 							existingFile = fileRef
 							break
 					if existingFile?
-						EditorController.replaceFile project_id, existingFile._id, path, "upload", callback
+						EditorController.replaceFile project_id, existingFile._id, path, "upload", user_id, callback
 					else
-						EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
+						EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", user_id, callback
 
 	addFolder: (user_id, project_id, folder_id, name, path, replace, callback = (error)-> ) ->
 		FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->

+ 5 - 1
services/web/app/views/project/editor/editor.pug

@@ -117,7 +117,11 @@ div.full-size(
 		h3.popover-title #{translate("link_sharing")}
 		.popover-content
 			p #{translate("try_out_link_sharing")}
-			img(src="/img/onboarding/linksharing/link-sharing.png" width="100%")
+			img(
+				src="/img/onboarding/linksharing/link-sharing.png"
+				alt="Link sharing demo"
+				width="100%"
+			)
 			p #{translate("try_link_sharing_description")}
 			button.btn.btn-default.btn-block(ng-click="dismiss()")
 				| #{translate("got_it")}

+ 1 - 1
services/web/app/views/project/editor/header.pug

@@ -9,7 +9,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
 		)
 			i.fa.fa-fw.fa-bars.editor-menu-icon
 			p.toolbar-label #{translate("menu")}
-		a(
+		a.toolbar-header-back-projects(
 			href="/project"
 		)
 			i.fa.fa-fw.fa-level-up

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

@@ -313,3 +313,59 @@ script(type="text/ng-template", id="userProfileModalTemplate")
 
 	.modal-footer
 		button.btn.btn-info(ng-click="done()") #{translate("done")}
+
+script(type="text/ng-template", id="v1ImportModalTemplate")
+	.modal-header
+		button.close(ng-click="dismiss()") ×
+		h3 #{translate("import_project_to_v2")}
+
+	.modal-body.v1-import-wrapper
+		.v1-import-step-1(ng-show="step === 1")
+			img.v1-import-img(
+				src="/img/v1-import/v2-editor.png"
+				alt="The new V2 Editor."
+			)
+			h2.v1-import-title Try importing your project to V2!
+			p Some exciting copy about the new features:
+			ul
+				li Some stuff
+				li Some more stuff
+				li Yet more stuff
+
+		.v1-import-step-2(ng-show="step === 2")
+			div.v1-import-warning(aria-label="Warning symbol.")
+				i.fa.fa-exclamation-triangle
+			h2.v1-import-title #[strong Warning:] Overleaf V2 is in beta
+			p Once importing your project you will lose access to the some of the features of Overleaf V1. This includes the git bridge, journal integrations, WYSIWYG and linked files. We’re working on bringing these features to V2!
+			p Once you have imported a project to V2 you #[strong cannot go back to V1].
+			p Are you sure you want to import to V2?
+
+	.modal-footer.v1-import-footer
+		div(ng-show="step === 1")
+			if settings.overleaf && settings.overleaf.host
+				a.btn.btn-primary.v1-import-btn(
+					ng-href=settings.overleaf.host + "/{{project.id}}"
+				) #{translate("open_in_v1")}
+			button.btn.btn-primary.v1-import-btn(
+				ng-click="moveToConfirmation()"
+			) #{translate("import_to_v2")}
+		div(ng-show="step === 2")
+			form(
+				async-form="v1Import",
+				name="v1ImportForm",
+				action="{{'/overleaf/project/'+ project.id + '/import'}}"
+				method="POST"
+				ng-cloak
+			)
+				input(name='_csrf', type='hidden', value=csrfToken)
+				form-messages(for="v1ImportForm")
+				if settings.overleaf && settings.overleaf.host
+					a.btn.btn-primary.v1-import-btn(
+						ng-href=settings.overleaf.host + "/{{project.id}}"
+						ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.success}"
+					) #{translate("never_mind_open_in_v1")}
+				input.btn.btn-primary.v1-import-btn(
+					type="submit",
+					value=translate('yes_im_sure')
+					ng-disabled="v1ImportForm.inflight || v1ImportForm.success"
+				)

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

@@ -114,9 +114,9 @@
 					) #{translate("delete_forever")}
 
 .row.row-spaced
-	if noV1Connection
+	each warning in warnings
 		.col-xs-12
-			.alert.alert-warning No V1 Connection
+			.alert.alert-warning(role="alert")= warning
 
 	.col-xs-12
 		.card.card-thin.project-list-card

+ 4 - 5
services/web/app/views/project/list/v1-item.pug

@@ -5,11 +5,10 @@
 		tooltip-append-to-body="true"
 	)
 	span
-		if settings.overleaf && settings.overleaf.host
-			a.projectName(
-					href=settings.overleaf.host + "/{{project.id}}"
-				stop-propagation="click"
-			) {{project.name}}
+		button.btn.btn-link.v1ProjectName(
+			ng-click="openV1ImportModal(project)"
+			stop-propagation="click"
+		) {{project.name}}
 
 .col-xs-2
 	span.owner {{ownerName()}}

+ 1 - 1
services/web/config/settings.defaults.coffee

@@ -111,7 +111,7 @@ module.exports = settings =
 		trackchanges:
 			url : "http://localhost:3015"
 		project_history:
-			enabled: false
+			enabled: process.env.PROJECT_HISTORY_ENABLED == 'true' or false
 			url : "http://localhost:3054"
 		docstore:
 			url : "http://#{process.env['DOCSTORE_HOST'] or 'localhost'}:3016"

+ 1 - 0
services/web/docker-compose.yml

@@ -24,6 +24,7 @@ services:
       REDIS_HOST: redis
       MONGO_URL: "mongodb://mongo/sharelatex"
       SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true'
+      PROJECT_HISTORY_ENABLED: 'true'
     depends_on:
       - redis
       - mongo

+ 2 - 1
services/web/docker-shared.template.yml

@@ -25,6 +25,7 @@ services:
       - ./config:/app/config
       - ./test/unit/coffee:/app/test/unit/coffee:ro
       - ./test/acceptance/coffee:/app/test/acceptance/coffee:ro
+      - ./test/acceptance/files:/app/test/acceptance/files:ro
       - ./test/smoke/coffee:/app/test/smoke/coffee:ro
       MODULE_VOLUMES
-    working_dir: /app
+    working_dir: /app

+ 10 - 0
services/web/public/coffee/main/project-list/modal-controllers.coffee

@@ -123,3 +123,13 @@ define [
 		$scope.onComplete = (error, name, response) ->
 			if response.project_id?
 				window.location = '/project/' + response.project_id
+
+	App.controller 'V1ImportModalController', ($scope, $modalInstance, project) ->
+		$scope.project = project
+		$scope.step = 1
+
+		$scope.dismiss = () ->
+			$modalInstance.dismiss('cancel')
+
+		$scope.moveToConfirmation = () ->
+			$scope.step = 2

+ 10 - 0
services/web/public/coffee/main/project-list/project-list.coffee

@@ -442,6 +442,16 @@ define [
 				path = "/project/#{selected_project_ids[0]}/download/zip"
 
 			window.location = path
+
+		$scope.openV1ImportModal = (project) ->
+			$modal.open(
+				templateUrl: 'v1ImportModalTemplate'
+				controller: 'V1ImportModalController'
+				size: 'lg'
+				windowClass: 'v1-import-modal'
+				resolve:
+					project: () -> project
+			)
 			
 		if storedUIOpts?.filter?
 			if storedUIOpts.filter == "tag" and storedUIOpts.selectedTagId?

+ 1 - 18
services/web/public/img/ol-brand/overleaf-o-grey.svg

@@ -1,18 +1 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-	 viewBox="0 0 136 157" style="enable-background:new 0 0 136 157;" xml:space="preserve">
-<style type="text/css">
-	.st0{fill:#9B9B9B;}
-</style>
-<g id="Page-1">
-	<g id="overleaf">
-		<g id="Group">
-			<path id="Fill-1" class="st0" d="M37.2,39.7C14.8,54,0,77.3,0,102.3C0,132.5,24.5,157,54.7,157c30.2,0,54.7-24.5,54.7-54.7
-				c0-23.3-14.6-43.3-35.2-51.1c-4-1.5-12.6-4.2-19.4-3.6c-9.8,6.2-21.8,19-27.4,31.8c8.4-10.1,21.5-14.5,33.2-12.6
-				c17.1,2.8,30.2,17.6,30.2,35.6c0,19.9-16.1,36-36,36c-11,0-20.8-4.9-27.4-12.6C17.5,114.3,15,101.9,17,89.8
-				c6.9-42.4,57.2-66.5,94.6-75.8C99.4,20.5,77.4,31.1,62,42.6c44.9,17.3,52.2-20.5,73.2-37.5C114-3.1,37.3-6.1,37.2,39.7z"/>
-		</g>
-	</g>
-</g>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 136 157"><path d="M37.2 39.7C14.8 54 0 77.3 0 102.3 0 132.5 24.5 157 54.7 157c30.2 0 54.7-24.5 54.7-54.7 0-23.3-14.6-43.3-35.2-51.1-4-1.5-12.6-4.2-19.4-3.6-9.8 6.2-21.8 19-27.4 31.8 8.4-10.1 21.5-14.5 33.2-12.6 17.1 2.8 30.2 17.6 30.2 35.6 0 19.9-16.1 36-36 36-11 0-20.8-4.9-27.4-12.6-9.9-11.5-12.4-23.9-10.4-36 6.9-42.4 57.2-66.5 94.6-75.8C99.4 20.5 77.4 31.1 62 42.6c44.9 17.3 52.2-20.5 73.2-37.5-21.2-8.2-97.9-11.2-98 34.6z" fill="#9b9b9b"/></svg>

+ 1 - 18
services/web/public/img/ol-brand/overleaf-o-white.svg

@@ -1,18 +1 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-	 viewBox="0 0 136 157" style="enable-background:new 0 0 136 157;" xml:space="preserve">
-<style type="text/css">
-	.st0{fill:#FFFFFF;}
-</style>
-<g id="Page-1">
-	<g id="overleaf">
-		<g id="Group">
-			<path id="Fill-1" class="st0" d="M37.2,39.7C14.8,54,0,77.3,0,102.3C0,132.5,24.5,157,54.7,157s54.7-24.5,54.7-54.7
-				c0-23.3-14.6-43.3-35.2-51.1c-4-1.5-12.6-4.2-19.4-3.6C45,53.8,33,66.6,27.4,79.4c8.4-10.1,21.5-14.5,33.2-12.6
-				c17.1,2.8,30.2,17.6,30.2,35.6c0,19.9-16.1,36-36,36c-11,0-20.8-4.9-27.4-12.6c-9.9-11.5-12.4-23.9-10.4-36
-				c6.9-42.4,57.2-66.5,94.6-75.8C99.4,20.5,77.4,31.1,62,42.6c44.9,17.3,52.2-20.5,73.2-37.5C114-3.1,37.3-6.1,37.2,39.7z"/>
-		</g>
-	</g>
-</g>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 136 157"><path d="M37.2 39.7C14.8 54 0 77.3 0 102.3 0 132.5 24.5 157 54.7 157s54.7-24.5 54.7-54.7c0-23.3-14.6-43.3-35.2-51.1-4-1.5-12.6-4.2-19.4-3.6-9.8 6.2-21.8 19-27.4 31.8 8.4-10.1 21.5-14.5 33.2-12.6 17.1 2.8 30.2 17.6 30.2 35.6 0 19.9-16.1 36-36 36-11 0-20.8-4.9-27.4-12.6-9.9-11.5-12.4-23.9-10.4-36 6.9-42.4 57.2-66.5 94.6-75.8C99.4 20.5 77.4 31.1 62 42.6c44.9 17.3 52.2-20.5 73.2-37.5-21.2-8.2-97.9-11.2-98 34.6z" fill="#fff"/></svg>

+ 1 - 10
services/web/public/img/ol-brand/overleaf-o.svg

@@ -1,10 +1 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="136px" height="157px" viewBox="0 0 136 157" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-  <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-    <g id="overleaf" fill="#4F9C45">
-      <g id="Group">
-        <path d="M37.205,39.652 C14.822,53.982 0,77.339 0,102.326 C0,132.522 24.48,157 54.681,157 C84.879,157 109.355,132.522 109.355,102.326 C109.355,78.986 94.729,59.05 74.151,51.215 C70.193,49.71 61.595,47.002 54.73,47.58 C44.924,53.814 32.979,66.624 27.319,79.389 C35.735,69.296 48.856,64.901 60.489,66.77 C77.615,69.547 90.697,84.408 90.697,102.321 C90.697,122.217 74.571,138.342 54.681,138.342 C43.719,138.342 33.896,133.446 27.293,125.723 C17.516,114.299 15,101.91 16.975,89.809 C23.902,47.434 74.208,23.279 111.611,14.01 C99.404,20.468 77.384,31.084 61.985,42.64 C106.909,59.981 114.169,22.123 135.202,5.181 C114.038,-3.07 37.33,-6.117 37.205,39.652 Z" id="Fill-1"></path>
-      </g>
-    </g>
-  </g>
-</svg>
+<svg width="136" height="157" xmlns="http://www.w3.org/2000/svg"><path d="M37.205 39.652C14.822 53.982 0 77.339 0 102.326 0 132.522 24.48 157 54.681 157c30.198 0 54.674-24.478 54.674-54.674 0-23.34-14.626-43.276-35.204-51.111-3.958-1.505-12.556-4.213-19.421-3.635-9.806 6.234-21.751 19.044-27.411 31.809 8.416-10.093 21.537-14.488 33.17-12.619 17.126 2.777 30.208 17.638 30.208 35.551 0 19.896-16.126 36.021-36.016 36.021-10.962 0-20.785-4.896-27.388-12.619C17.516 114.299 15 101.91 16.975 89.809c6.927-42.375 57.233-66.53 94.636-75.799-12.207 6.458-34.227 17.074-49.626 28.63 44.924 17.341 52.184-20.517 73.217-37.459C114.038-3.07 37.33-6.117 37.205 39.652z" fill="#4F9C45" fill-rule="evenodd"/></svg>

BIN
services/web/public/img/v1-import/v2-editor.png


+ 1 - 1
services/web/public/stylesheets/app/editor/file-tree.less

@@ -52,7 +52,7 @@
 		}
 
 		li {
-			line-height: 2.6;
+			line-height: @file-tree-line-height;
 			position: relative;
 
 			.entity {

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

@@ -27,6 +27,10 @@
 		color: @toolbar-icon-btn-color;
 		padding: 0 5px;
 		border-radius: @border-radius-small;
+		&.toolbar-header-back-projects {
+			padding: 5px 10px 4px;
+			margin-bottom: 1px;
+		}
 		&:hover {
 			text-shadow: @toolbar-icon-btn-hover-shadow;
 			color: @toolbar-icon-btn-hover-color;
@@ -38,7 +42,7 @@
 			}
 			color: white;
 			background-color: @link-color;
-			.box-shadow(inset 0 3px 5px rgba(0, 0, 0, 0.225));
+			box-shadow: @toolbar-icon-btn-hover-boxshadow;
 			&:hover {
 				color: white;
 			}
@@ -124,7 +128,6 @@
 	}
 }
 
-
 .toolbar-small-mixin() {
 	height: 32px;
 }

+ 24 - 0
services/web/public/stylesheets/app/list/v1-import-modal.less

@@ -0,0 +1,24 @@
+.v1-import-title {
+	text-align: center;
+}
+
+.v1-import-img {
+	width: 100%;
+}
+
+.v1-import-warning {
+	text-align: center;
+	color: #fdce02;
+	font-size: 14em;
+	line-height: 1em;
+}
+
+.v1-import-footer {
+	display: flex;
+	justify-content: space-evenly;
+	text-align: left;
+}
+
+	.v1-import-btn {
+		width: 20rem;
+	}

+ 6 - 0
services/web/public/stylesheets/app/project-list.less

@@ -1,3 +1,5 @@
+@import "./list/v1-import-modal.less";
+
 @announcements-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
 
 @keyframes pulse {
@@ -332,6 +334,10 @@ ul.project-list {
 		.projectName {
 			margin-right: @line-height-computed / 4;
 		}
+		.v1ProjectName {
+			margin-right: @line-height-computed / 4;
+			padding: 0;
+		}
 
 		.tag-label {
 			margin-left: @line-height-computed / 4;

+ 1 - 0
services/web/public/stylesheets/core/_common-variables.less

@@ -923,6 +923,7 @@
 @editor-toggler-hover-bg-color    : #DDD;
 @synctex-controls-z-index         : 3;
 @synctex-controls-padding         : 0 2px;
+
 // Tags
 @tag-border-radius  : 0.25em;
 @tag-bg-color       : @label-default-bg;

+ 307 - 0
services/web/test/acceptance/coffee/ProjectStructureTests.coffee

@@ -0,0 +1,307 @@
+async = require "async"
+expect = require("chai").expect
+mkdirp = require "mkdirp"
+ObjectId = require("mongojs").ObjectId
+Path = require "path"
+fs = require "fs"
+Settings = require "settings-sharelatex"
+_ = require "underscore"
+
+ProjectGetter = require "../../../app/js/Features/Project/ProjectGetter.js"
+
+MockDocUpdaterApi = require './helpers/MockDocUpdaterApi'
+MockFileStoreApi = require './helpers/MockFileStoreApi'
+MockProjectHistoryApi = require './helpers/MockProjectHistoryApi'
+request = require "./helpers/request"
+User = require "./helpers/User"
+
+describe "ProjectStructureChanges", ->
+	before (done) ->
+		@owner = new User()
+		@owner.login done
+
+	describe "creating a project from the example template", ->
+		before (done) ->
+			MockDocUpdaterApi.clearProjectStructureUpdates()
+			@owner.createProject "example-project", {template: "example"}, (error, project_id) =>
+				throw error if error?
+				@example_project_id = project_id
+				done()
+
+		it "should version creating a doc", ->
+			updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
+			expect(updates.length).to.equal(2)
+			_.each updates, (update) =>
+				expect(update.userId).to.equal(@owner._id)
+				expect(update.docLines).to.be.a('string')
+			expect(_.where(updates, pathname: "/main.tex").length).to.equal 1
+			expect(_.where(updates, pathname: "/references.bib").length).to.equal 1
+
+		it "should version creating a file", ->
+			updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
+			expect(updates.length).to.equal(1)
+			update = updates[0]
+			expect(update.userId).to.equal(@owner._id)
+			expect(update.pathname).to.equal("/universe.jpg")
+			expect(update.url).to.be.a('string');
+
+	describe "duplicating a project", ->
+		before (done) ->
+			MockDocUpdaterApi.clearProjectStructureUpdates()
+			@owner.request.post {
+				uri: "/Project/#{@example_project_id}/clone",
+				json:
+					projectName: 'new.tex'
+			}, (error, res, body) =>
+				throw error if error?
+				if res.statusCode < 200 || res.statusCode >= 300
+					throw new Error("failed to add doc #{res.statusCode}")
+				@dup_project_id = body.project_id
+				done()
+
+		it "should version the dosc created", ->
+			updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).docUpdates
+			expect(updates.length).to.equal(2)
+			_.each updates, (update) =>
+				expect(update.userId).to.equal(@owner._id)
+				expect(update.docLines).to.be.a('string')
+			expect(_.where(updates, pathname: "/main.tex").length).to.equal(1)
+			expect(_.where(updates, pathname: "/references.bib").length).to.equal(1)
+
+		it "should version the files created", ->
+			updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).fileUpdates
+			expect(updates.length).to.equal(1)
+			update = updates[0]
+			expect(update.userId).to.equal(@owner._id)
+			expect(update.pathname).to.equal("/universe.jpg")
+			expect(update.url).to.be.a('string');
+
+	describe "adding a doc", ->
+		before (done) ->
+			MockDocUpdaterApi.clearProjectStructureUpdates()
+
+			ProjectGetter.getProject @example_project_id, (error, projects) =>
+				throw error if error?
+				@owner.request.post {
+					uri: "project/#{@example_project_id}/doc",
+					json:
+						name: 'new.tex'
+						parent_folder_id: projects[0].rootFolder[0]._id
+				}, (error, res, body) =>
+					throw error if error?
+					if res.statusCode < 200 || res.statusCode >= 300
+						throw new Error("failed to add doc #{res.statusCode}")
+					done()
+
+		it "should version the doc added", ->
+			updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
+			expect(updates.length).to.equal(1)
+			update = updates[0]
+			expect(update.userId).to.equal(@owner._id)
+			expect(update.pathname).to.equal("/new.tex")
+			expect(update.docLines).to.be.a('string');
+
+	describe "uploading a project", ->
+		before (done) ->
+			MockDocUpdaterApi.clearProjectStructureUpdates()
+
+			zip_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test_project.zip'))
+
+			req = @owner.request.post {
+				uri: "project/new/upload",
+				formData:
+					qqfile: zip_file
+			}, (error, res, body) =>
+				throw error if error?
+				if res.statusCode < 200 || res.statusCode >= 300
+					throw new Error("failed to upload project #{res.statusCode}")
+				@uploaded_project_id = JSON.parse(body).project_id
+				done()
+
+		it "should version the dosc created", ->
+			updates = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id).docUpdates
+			expect(updates.length).to.equal(1)
+			update = updates[0]
+			expect(update.userId).to.equal(@owner._id)
+			expect(update.pathname).to.equal("/main.tex")
+			expect(update.docLines).to.equal("Test")
+
+		it "should version the files created", ->
+			updates = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id).fileUpdates
+			expect(updates.length).to.equal(1)
+			update = updates[0]
+			expect(update.userId).to.equal(@owner._id)
+			expect(update.pathname).to.equal("/1pixel.png")
+			expect(update.url).to.be.a('string');
+
+	describe "uploading a file", ->
+		before (done) ->
+			ProjectGetter.getProject @example_project_id, (error, projects) =>
+				throw error if error?
+				@root_folder_id = projects[0].rootFolder[0]._id.toString()
+				done()
+
+		beforeEach () ->
+			MockDocUpdaterApi.clearProjectStructureUpdates()
+
+		it "should version a newly uploaded file", (done) ->
+			image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/1pixel.png'))
+
+			req = @owner.request.post {
+				uri: "project/#{@example_project_id}/upload",
+				qs:
+					folder_id: @root_folder_id
+				formData:
+					qqfile:
+						value: image_file
+						options:
+							filename: '1pixel.png',
+							contentType: 'image/png'
+			}, (error, res, body) =>
+				throw error if error?
+				if res.statusCode < 200 || res.statusCode >= 300
+					throw new Error("failed to upload file #{res.statusCode}")
+
+				updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
+				expect(updates.length).to.equal(1)
+				update = updates[0]
+				expect(update.userId).to.equal(@owner._id)
+				expect(update.pathname).to.equal("/1pixel.png")
+				expect(update.url).to.be.a('string');
+				@original_file_url = update.url
+
+				done()
+
+		it "should version a replacement file", (done) ->
+			image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/2pixel.png'))
+
+			req = @owner.request.post {
+				uri: "project/#{@example_project_id}/upload",
+				qs:
+					folder_id: @root_folder_id
+				formData:
+					qqfile:
+						value: image_file
+						options:
+							filename: '1pixel.png',
+							contentType: 'image/png'
+			}, (error, res, body) =>
+				throw error if error?
+				if res.statusCode < 200 || res.statusCode >= 300
+					throw new Error("failed to upload file #{res.statusCode}")
+
+				updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
+				expect(updates.length).to.equal(1)
+				update = updates[0]
+				expect(update.userId).to.equal(@owner._id)
+				expect(update.pathname).to.equal("/1pixel.png")
+				expect(update.url).to.be.a('string');
+
+				done()
+
+	describe "tpds", ->
+		before (done) ->
+			@tpds_project_name = "tpds-project-#{new ObjectId().toString()}"
+			@owner.createProject @tpds_project_name, (error, project_id) =>
+				throw error if error?
+				@tpds_project_id = project_id
+				mkdirp Settings.path.dumpFolder, done
+
+		beforeEach () ->
+			MockDocUpdaterApi.clearProjectStructureUpdates()
+
+		it "should version adding a doc", (done) ->
+			tex_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test.tex'))
+
+			req = @owner.request.post {
+				uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/test.tex",
+				auth:
+					user: _.keys(Settings.httpAuthUsers)[0]
+					pass: _.values(Settings.httpAuthUsers)[0]
+					sendImmediately: true
+			}
+
+			tex_file.on "error", (err) ->
+				throw err
+
+			req.on "error", (err) ->
+				throw err
+
+			req.on "response", (res) =>
+				if res.statusCode < 200 || res.statusCode >= 300
+					throw new Error("failed to upload file #{res.statusCode}")
+
+				updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).docUpdates
+				expect(updates.length).to.equal(1)
+				update = updates[0]
+				expect(update.userId).to.equal(@owner._id)
+				expect(update.pathname).to.equal("/test.tex")
+				expect(update.docLines).to.equal("Test")
+
+				done()
+
+			tex_file.pipe(req)
+
+		it "should version adding a new file", (done) ->
+			image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/1pixel.png'))
+
+			req = @owner.request.post {
+				uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/1pixel.png",
+				auth:
+					user: _.keys(Settings.httpAuthUsers)[0]
+					pass: _.values(Settings.httpAuthUsers)[0]
+					sendImmediately: true
+			}
+
+			image_file.on "error", (err) ->
+				throw err
+
+			req.on "error", (err) ->
+				throw err
+
+			req.on "response", (res) =>
+				if res.statusCode < 200 || res.statusCode >= 300
+					throw new Error("failed to upload file #{res.statusCode}")
+
+				updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).fileUpdates
+				expect(updates.length).to.equal(1)
+				update = updates[0]
+				expect(update.userId).to.equal(@owner._id)
+				expect(update.pathname).to.equal("/1pixel.png")
+				expect(update.url).to.be.a('string');
+
+				done()
+
+			image_file.pipe(req)
+
+		it "should version replacing a file", (done) ->
+			image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/2pixel.png'))
+
+			req = @owner.request.post {
+				uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/1pixel.png",
+				auth:
+					user: _.keys(Settings.httpAuthUsers)[0]
+					pass: _.values(Settings.httpAuthUsers)[0]
+					sendImmediately: true
+			}
+
+			image_file.on "error", (err) ->
+				throw err
+
+			req.on "error", (err) ->
+				throw err
+
+			req.on "response", (res) =>
+				if res.statusCode < 200 || res.statusCode >= 300
+					throw new Error("failed to upload file #{res.statusCode}")
+
+				updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).fileUpdates
+				expect(updates.length).to.equal(1)
+				update = updates[0]
+				expect(update.userId).to.equal(@owner._id)
+				expect(update.pathname).to.equal("/1pixel.png")
+				expect(update.url).to.be.a('string');
+
+				done()
+
+			image_file.pipe(req)

+ 27 - 0
services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee

@@ -1,11 +1,38 @@
 express = require("express")
 app = express()
+bodyParser = require "body-parser"
+jsonParser = bodyParser.json()
 
 module.exports = MockDocUpdaterApi =
+	updates: {}
+
+	clearProjectStructureUpdates: () ->
+		@updates = {}
+
+	getProjectStructureUpdates: (project_id) ->
+		@updates[project_id] || { docUpdates: [], fileUpdates: [] }
+
+	addProjectStructureUpdates: (project_id, userId, docUpdates, fileUpdates) ->
+		@updates[project_id] ||= { docUpdates: [], fileUpdates: [] }
+
+		for update in docUpdates
+			update.userId = userId
+			@updates[project_id].docUpdates.push(update)
+
+		for update in fileUpdates
+			update.userId = userId
+			@updates[project_id].fileUpdates.push(update)
+
 	run: () ->
 		app.post "/project/:project_id/flush", (req, res, next) =>
 			res.sendStatus 200
 
+		app.post "/project/:project_id", jsonParser, (req, res, next) =>
+			project_id = req.params.project_id
+			{userId, docUpdates, fileUpdates} = req.body
+			@addProjectStructureUpdates(project_id, userId, docUpdates, fileUpdates)
+			res.sendStatus 200
+
 		app.listen 3003, (error) ->
 			throw error if error?
 		.on "error", (error) ->

+ 1 - 0
services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee

@@ -13,6 +13,7 @@ module.exports = MockDocStoreApi =
 			@docs[project_id][doc_id] = {lines, version, ranges}
 			@docs[project_id][doc_id].rev ?= 0
 			@docs[project_id][doc_id].rev += 1
+			@docs[project_id][doc_id]._id = doc_id
 			res.json {
 				modified: true
 				rev: @docs[project_id][doc_id].rev

+ 20 - 0
services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee

@@ -0,0 +1,20 @@
+express = require("express")
+app = express()
+
+module.exports = MockFileStoreApi =
+	files: {}
+
+	run: () ->
+		app.post "/project/:project_id/file/:file_id", (req, res, next) =>
+			req.on 'data', ->
+
+			req.on 'end', ->
+				res.send 200
+
+		app.listen 3009, (error) ->
+			throw error if error?
+		.on "error", (error) ->
+			console.error "error starting MockFileStoreApi:", error.message
+			process.exit(1)
+
+MockFileStoreApi.run()

+ 18 - 0
services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee

@@ -0,0 +1,18 @@
+express = require("express")
+app = express()
+
+module.exports = MockProjectHistoryApi =
+	docs: {}
+
+	run: () ->
+		app.post "/project", (req, res, next) =>
+			res.json project: id: 1
+
+		app.listen 3054, (error) ->
+			throw error if error?
+		.on "error", (error) ->
+			console.error "error starting MockProjectHistoryApi:", error.message
+			process.exit(1)
+
+
+MockProjectHistoryApi.run()

+ 6 - 3
services/web/test/acceptance/coffee/helpers/User.coffee

@@ -99,11 +99,14 @@ class User
 	getProject: (project_id, callback = (error, project)->) ->
 		db.projects.findOne {_id: ObjectId(project_id.toString())}, callback
 
-	createProject: (name, callback = (error, project_id) ->) ->
+	createProject: (name, options, callback = (error, oroject_id) ->) ->
+		if typeof options == "function"
+			callback = options
+			options = {}
+
 		@request.post {
 			url: "/project/new",
-			json:
-				projectName: name
+			json: Object.assign({projectName: name}, options)
 		}, (error, response, body) ->
 			return callback(error) if error?
 			if !body?.project_id?

BIN
services/web/test/acceptance/files/1pixel.png


BIN
services/web/test/acceptance/files/2pixel.png


+ 1 - 0
services/web/test/acceptance/files/test.tex

@@ -0,0 +1 @@
+Test

BIN
services/web/test/acceptance/files/test_project.zip


+ 1 - 1
services/web/test/acceptance/scripts/full-test.sh

@@ -3,7 +3,7 @@
 # If you're running on OS X, you probably need to rebuild
 # some dependencies in the docker container, before it will start.
 #
-# npm rebuild --update-binary
+#npm rebuild --update-binary
 
 echo ">> Starting server..."
 

+ 80 - 28
services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee

@@ -390,26 +390,13 @@ describe 'DocumentUpdaterHandler', ->
 	describe "updateProjectStructure ", ->
 		beforeEach ->
 			@user_id = 1234
-			@docIdA = new ObjectId()
-			@docIdB = new ObjectId()
-			@oldDocs = [
-				{ path: '/old_a', doc: _id: @docIdA }
-				{ path: '/old_b', doc: _id: @docIdB }
-			]
-			# create new instances of the same ObjectIds so that == doens't pass
-			@newDocs = [
-				{ path: '/old_a', doc: _id: new ObjectId(@docIdA.toString()) }
-				{ path: '/new_b', doc: _id: new ObjectId(@docIdB.toString()) }
-			]
-			@oldFiles = []
-			@newFiles = []
 
 		describe "with project history disabled", ->
 			beforeEach ->
 				@settings.apis.project_history.enabled = false
 				@request.post = sinon.stub()
 
-				@handler.updateProjectStructure @project_id, @user_id, @oldDocs, @newDocs, @oldFiles, @newFiles, @callback
+				@handler.updateProjectStructure @project_id, @user_id, {}, @callback
 
 			it 'does not make a web request', ->
 				@request.post.called.should.equal false
@@ -420,20 +407,85 @@ describe 'DocumentUpdaterHandler', ->
 		describe "with project history enabled", ->
 			beforeEach ->
 				@settings.apis.project_history.enabled = true
+				@url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}"
 				@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "")
-				@handler.updateProjectStructure @project_id, @user_id, @oldDocs, @newDocs, @oldFiles, @newFiles, @callback
 
-			it 'should send the structure update to the document updater', ->
-				docUpdates = [
-					id: @docIdB,
-					pathname: "/old_b"
-					newPathname: "/new_b"
-				]
+			describe "when an entity has changed name", ->
+				it 'should send the structure update to the document updater', (done) ->
+					@docIdA = new ObjectId()
+					@docIdB = new ObjectId()
+					@changes = {
+						oldDocs: [
+							{ path: '/old_a', doc: _id: @docIdA }
+							{ path: '/old_b', doc: _id: @docIdB }
+						]
+						# create new instances of the same ObjectIds so that == doesn't pass
+						newDocs: [
+							{ path: '/old_a', doc: _id: new ObjectId(@docIdA.toString()) }
+							{ path: '/new_b', doc: _id: new ObjectId(@docIdB.toString()) }
+						]
+					}
+
+					docUpdates = [
+						id: @docIdB.toString(),
+						pathname: "/old_b"
+						newPathname: "/new_b"
+					]
+
+					@handler.updateProjectStructure @project_id, @user_id, @changes, () =>
+						@request.post
+							.calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id})
+							.should.equal true
+						done()
+
+			describe "when a doc has been added", ->
+				it 'should send the structure update to the document updater', (done) ->
+					@docId = new ObjectId()
+					@changes = newDocs: [
+						{ path: '/foo', docLines: 'a\nb', doc: _id: @docId }
+					]
+
+					docUpdates = [
+						id: @docId.toString(),
+						pathname: "/foo"
+						docLines: 'a\nb'
+						url: undefined
+					]
+
+					@handler.updateProjectStructure @project_id, @user_id, @changes, () =>
+						@request.post
+							.calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id})
+							.should.equal true
+						done()
+
+			describe "when a file has been added", ->
+				it 'should send the structure update to the document updater', (done) ->
+					@fileId = new ObjectId()
+					@changes = newFiles: [
+						{ path: '/bar', url: 'filestore.example.com/file', file: _id: @fileId }
+					]
+
+					fileUpdates = [
+						id: @fileId.toString(),
+						pathname: "/bar"
+						url: 'filestore.example.com/file'
+						docLines: undefined
+					]
+
+					@handler.updateProjectStructure @project_id, @user_id, @changes, () =>
+						@request.post
+							.calledWith(url: @url, json: {docUpdates: [], fileUpdates, userId: @user_id})
+							.should.equal true
+						done()
+
+			describe "when a doc has been deleted", ->
+				it 'should do nothing', (done) ->
+					@docId = new ObjectId()
+					@changes = oldDocs: [
+						{ path: '/foo', docLines: 'a\nb', doc: _id: @docId }
+					]
+
+					@handler.updateProjectStructure @project_id, @user_id, @changes, () =>
+						@request.post.called.should.equal false
+						done()
 
-				url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}"
-				@request.post
-					.calledWith(url: url, json: {docUpdates, fileUpdates: [], userId: @user_id})
-					.should.equal true
-
-			it "should call the callback with no error", ->
-				@callback.calledWith(null).should.equal true

+ 31 - 34
services/web/test/unit/coffee/Editor/EditorControllerTests.coffee

@@ -131,24 +131,24 @@ describe "EditorController", ->
 			@docLines = ["1234","dskl"]
 
 		it 'should add the doc using the project entity handler', (done)->
-			mock = sinon.mock(@ProjectEntityHandler).expects("addDoc").withArgs(@project_id, @folder_id, @docName, @docLines).callsArg(4)
+			mock = sinon.mock(@ProjectEntityHandler).expects("addDoc").withArgs(@project_id, @folder_id, @docName, @docLines).callsArg(5)
 
-			@EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, ->
+			@EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, @user_id, ->
 				mock.verify()
 				done()
 
 		it 'should send the update out to the users in the project', (done)->
-			@ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(4, null, @doc, @folder_id)
+			@ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(5, null, @doc, @folder_id)
 
-			@EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, =>
+			@EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, @user_id, =>
 				@EditorRealTimeController.emitToRoom
 					.calledWith(@project_id, "reciveNewDoc", @folder_id, @doc, @source)
 					.should.equal true
 				done()
 
 		it 'should return the doc to the callback', (done) ->
-			@ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(4, null, @doc, @folder_id)
-			@EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, (error, doc) =>
+			@ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(5, null, @doc, @folder_id)
+			@EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, @user_id, (error, doc) =>
 				doc.should.equal @doc
 				done()
 
@@ -157,32 +157,29 @@ describe "EditorController", ->
 		beforeEach ->
 			@LockManager.getLock.callsArgWith(1)
 			@LockManager.releaseLock.callsArgWith(1)
-			@EditorController.addDocWithoutLock = sinon.stub().callsArgWith(5)
+			@EditorController.addDocWithoutLock = sinon.stub().callsArgWith(6)
 
 		it "should call addDocWithoutLock", (done)->
-			@EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, =>
-				@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @docName, @docLines, @source).should.equal true
+			@EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, =>
+				@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @docName, @docLines, @source, @user_id).should.equal true
 				done()
 
 		it "should take the lock", (done)->
-			@EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, =>
+			@EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, =>
 				@LockManager.getLock.calledWith(@project_id).should.equal true
 				done()
 
 		it "should release the lock", (done)->
-			@EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, =>
+			@EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, =>
 				@LockManager.releaseLock.calledWith(@project_id).should.equal true
 				done()
 
 		it "should error if it can't cat the lock", (done)->
 			@LockManager.getLock = sinon.stub().callsArgWith(1, "timed out")
-			@EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, (err)=>
+			@EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, (err)=>
 				expect(err).to.exist
 				err.should.equal "timed out"
-				done()			
-
-
-
+				done()
 
 	describe 'addFileWithoutLock:', ->
 		beforeEach ->
@@ -196,23 +193,23 @@ describe "EditorController", ->
 			@stream = new ArrayBuffer()
 
 		it 'should add the folder using the project entity handler', (done)->
-			@ProjectEntityHandler.addFile = sinon.stub().callsArgWith(4)
-			@EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, =>
-				@ProjectEntityHandler.addFile.calledWith(@project_id, @folder_id).should.equal true
+			@ProjectEntityHandler.addFile = sinon.stub().callsArgWith(5)
+			@EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, @user_id, =>
+				@ProjectEntityHandler.addFile.calledWith(@project_id, @folder_id, @fileName, @stream, @user_id).should.equal true
 				done()
 
 		it 'should send the update of a new folder out to the users in the project', (done)->
-			@ProjectEntityHandler.addFile = sinon.stub().callsArgWith(4, null, @file, @folder_id)
+			@ProjectEntityHandler.addFile = sinon.stub().callsArgWith(5, null, @file, @folder_id)
 
-			@EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, =>
+			@EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, @user_id, =>
 				@EditorRealTimeController.emitToRoom
 					.calledWith(@project_id, "reciveNewFile", @folder_id, @file, @source)
 					.should.equal true
 				done()
 
 		it "should return the file in the callback", (done) ->
-			@ProjectEntityHandler.addFile = sinon.stub().callsArgWith(4, null, @file, @folder_id)
-			@EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, (error, file) =>
+			@ProjectEntityHandler.addFile = sinon.stub().callsArgWith(5, null, @file, @folder_id)
+			@EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) =>
 				file.should.equal @file
 				done()
 
@@ -222,28 +219,28 @@ describe "EditorController", ->
 		beforeEach ->
 			@LockManager.getLock.callsArgWith(1)
 			@LockManager.releaseLock.callsArgWith(1)
-			@EditorController.addFileWithoutLock = sinon.stub().callsArgWith(5)
+			@EditorController.addFileWithoutLock = sinon.stub().callsArgWith(6)
 
 		it "should call addFileWithoutLock", (done)->
-			@EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, (error, file) =>
-				@EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @fileName, @stream, @source).should.equal true
+			@EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) =>
+				@EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @fileName, @stream, @source, @user_id).should.equal true
 				done()
 
 		it "should take the lock", (done)->
-			@EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, (error, file) =>
+			@EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) =>
 				@LockManager.getLock.calledWith(@project_id).should.equal true
 				done()
 
 		it "should release the lock", (done)->
-			@EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, (error, file) =>
+			@EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) =>
 				@LockManager.releaseLock.calledWith(@project_id).should.equal true
 				done()
 
 		it "should error if it can't cat the lock", (done)->
 			@LockManager.getLock = sinon.stub().callsArgWith(1, "timed out")
-			@EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, (err, file) =>
-				expect(err).to.exist
-				err.should.equal "timed out"
+			@EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) =>
+				expect(error).to.exist
+				error.should.equal "timed out"
 				done()			
 
 
@@ -256,9 +253,9 @@ describe "EditorController", ->
 			@fsPath = "/folder/file.png"
 
 		it 'should send the replace file message to the editor controller', (done)->
-			@ProjectEntityHandler.replaceFile = sinon.stub().callsArgWith(3)
-			@EditorController.replaceFile @project_id, @file_id, @fsPath, @source, =>
-				@ProjectEntityHandler.replaceFile.calledWith(@project_id, @file_id, @fsPath).should.equal true
+			@ProjectEntityHandler.replaceFile = sinon.stub().callsArgWith(4)
+			@EditorController.replaceFile @project_id, @file_id, @fsPath, @source, @user_id, =>
+				@ProjectEntityHandler.replaceFile.calledWith(@project_id, @file_id, @fsPath, @user_id).should.equal true
 				done()
 
 	describe 'addFolderWithoutLock :', ->

+ 2 - 2
services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee

@@ -201,7 +201,7 @@ describe "EditorHttpController", ->
 			@req.body =
 				name: @name = "doc-name"
 				parent_folder_id: @parent_folder_id
-			@EditorController.addDoc = sinon.stub().callsArgWith(5, null, @doc)
+			@EditorController.addDoc = sinon.stub().callsArgWith(6, null, @doc)
 
 		describe "successfully", ->
 			beforeEach ->
@@ -209,7 +209,7 @@ describe "EditorHttpController", ->
 
 			it "should call EditorController.addDoc", ->
 				@EditorController.addDoc
-					.calledWith(@project_id, @parent_folder_id, @name, [], "editor")
+					.calledWith(@project_id, @parent_folder_id, @name, [], "editor", @userId)
 					.should.equal true
 
 			it "should send the doc back as JSON", ->

+ 16 - 10
services/web/test/unit/coffee/FileStore/FileStoreHandlerTests.coffee

@@ -16,7 +16,7 @@ describe "FileStoreHandler", ->
 			})
 		@writeStream =
 			my:"writeStream"
-			on: (type, cb)-> 
+			on: (type, cb)->
 				if type == "response"
 					cb({statusCode: 200})
 		@readStream = {my:"readStream", on: sinon.stub()}
@@ -38,7 +38,7 @@ describe "FileStoreHandler", ->
 			@isSafeOnFileSystem = true
 
 		it "should create read stream", (done)->
-			@fs.createReadStream.returns 
+			@fs.createReadStream.returns
 				pipe:->
 				on: (type, cb)->
 					if type == "open"
@@ -49,8 +49,8 @@ describe "FileStoreHandler", ->
 
 		it "should pipe the read stream to request", (done)->
 			@request.returns(@writeStream)
-			@fs.createReadStream.returns 
-				on: (type, cb)-> 
+			@fs.createReadStream.returns
+				on: (type, cb)->
 					if type == "open"
 						cb()
 				pipe:(o)=>
@@ -59,9 +59,9 @@ describe "FileStoreHandler", ->
 			@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, =>
 
 		it "should pass the correct options to request", (done)->
-			@fs.createReadStream.returns 
+			@fs.createReadStream.returns
 				pipe:->
-				on: (type, cb)-> 
+				on: (type, cb)->
 					if type == "open"
 						cb()
 			@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, =>
@@ -70,23 +70,24 @@ describe "FileStoreHandler", ->
 				done()
 
 		it "builds the correct url", (done)->
-			@fs.createReadStream.returns 
+			@fs.createReadStream.returns
 				pipe:->
-				on: (type, cb)-> 
+				on: (type, cb)->
 					if type == "open"
 						cb()
 			@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, =>
 				@handler._buildUrl.calledWith(@project_id, @file_id).should.equal true
 				done()
 
-		it 'should callback with null', (done) ->
+		it 'should callback with the url', (done) ->
 			@fs.createReadStream.returns
 				pipe:->
 				on: (type, cb)->
 					if type == "open"
 						cb()
-			@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, (err) =>
+			@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, (err, url) =>
 				expect(err).to.not.exist
+				expect(url).to.equal(@handler._buildUrl())
 				done()
 
 		describe "symlink", ->
@@ -218,6 +219,11 @@ describe "FileStoreHandler", ->
 				@handler._buildUrl.calledWith(@newProject_id, @newFile_id).should.equal true
 				done()
 
+		it "returns the url", (done)->
+			@request.callsArgWith(1, null)
+			@handler.copyFile @project_id, @file_id, @newProject_id, @newFile_id, (err, url) =>
+				url.should.equal "http://filestore.stubbedBuilder.com"
+				done()
 
 		it "should return the err", (done)->
 			error = "errrror"

+ 7 - 6
services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee

@@ -33,8 +33,8 @@ describe 'ProjectCreationHandler', ->
 			constructor:(options)->
 				{@name} = options
 		@ProjectEntityHandler =
-			addDoc: sinon.stub().callsArgWith(4, null, {_id: docId})
-			addFile: sinon.stub().callsArg(4)
+			addDoc: sinon.stub().callsArgWith(5, null, {_id: docId})
+			addFile: sinon.stub().callsArg(5)
 			setRootDoc: sinon.stub().callsArg(2)
 		@ProjectDetailsHandler =
 			validateProjectName: sinon.stub().yields()
@@ -149,7 +149,7 @@ describe 'ProjectCreationHandler', ->
 				.should.equal true
 
 		it 'should insert main.tex', ->
-			@ProjectEntityHandler.addDoc.calledWith(project_id, rootFolderId, "main.tex", ["mainbasic.tex", "lines"])
+			@ProjectEntityHandler.addDoc.calledWith(project_id, rootFolderId, "main.tex", ["mainbasic.tex", "lines"], ownerId)
 				.should.equal true
 
 		it 'should set the main doc id', ->
@@ -180,19 +180,20 @@ describe 'ProjectCreationHandler', ->
 
 		it 'should insert main.tex', ->
 			@ProjectEntityHandler.addDoc
-				.calledWith(project_id, rootFolderId, "main.tex", ["main.tex", "lines"])
+				.calledWith(project_id, rootFolderId, "main.tex", ["main.tex", "lines"], ownerId)
 				.should.equal true
 
 		it 'should insert references.bib', ->
 			@ProjectEntityHandler.addDoc
-				.calledWith(project_id, rootFolderId, "references.bib", ["references.bib", "lines"])
+				.calledWith(project_id, rootFolderId, "references.bib", ["references.bib", "lines"], ownerId)
 				.should.equal true
 
 		it 'should insert universe.jpg', ->
 			@ProjectEntityHandler.addFile
 				.calledWith(
 					project_id, rootFolderId, "universe.jpg",
-					Path.resolve(__dirname + "/../../../../app/templates/project_files/universe.jpg")
+					Path.resolve(__dirname + "/../../../../app/templates/project_files/universe.jpg"),
+					ownerId
 				)
 				.should.equal true
 

+ 22 - 10
services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee

@@ -64,8 +64,8 @@ describe 'ProjectDuplicator', ->
 		@projectOptionsHandler =
 			setCompiler : sinon.stub()								
 		@entityHandler =
-			addDocWithProject: sinon.stub().callsArgWith(4, null, {name:"somDoc"})
-			copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(4)
+			addDocWithProject: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
+			copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(5)
 			setRootDoc: sinon.stub()
 			addFolderWithProject: sinon.stub().callsArgWith(3, null, @newFolder)
 
@@ -112,13 +112,13 @@ describe 'ProjectDuplicator', ->
 			done()
 
 	it 'should use the same compiler', (done)->
-		@entityHandler.addDocWithProject.callsArgWith(4, null, @rootFolder.docs[0])
+		@entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
 		@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
 			@projectOptionsHandler.setCompiler.calledWith(@stubbedNewProject._id, @project.compiler).should.equal true
 			done()
 	
 	it 'should use the same root doc', (done)->
-		@entityHandler.addDocWithProject.callsArgWith(4, null, @rootFolder.docs[0])
+		@entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
 		@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
 			@entityHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true
 			done()
@@ -139,14 +139,26 @@ describe 'ProjectDuplicator', ->
 	it 'should copy all the docs', (done)->
 		@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
 			@DocstoreManager.getAllDocs.calledWith(@old_project_id).should.equal true
-			@entityHandler.addDocWithProject.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines).should.equal true
-			@entityHandler.addDocWithProject.calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines).should.equal true
-			@entityHandler.addDocWithProject.calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines).should.equal true
+			@entityHandler.addDocWithProject
+				.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines, @owner._id)
+				.should.equal true
+			@entityHandler.addDocWithProject
+				.calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines, @owner._id)
+				.should.equal true
+			@entityHandler.addDocWithProject
+				.calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines, @owner._id)
+				.should.equal true
 			done()
 
 	it 'should copy all the files', (done)->
 		@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
-			@entityHandler.copyFileFromExistingProjectWithProject.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @project._id, @rootFolder.fileRefs[0]).should.equal true
-			@entityHandler.copyFileFromExistingProjectWithProject.calledWith(@stubbedNewProject, @newFolder._id, @project._id, @level1folder.fileRefs[0]).should.equal true
-			@entityHandler.copyFileFromExistingProjectWithProject.calledWith(@stubbedNewProject, @newFolder._id, @project._id, @level2folder.fileRefs[0]).should.equal true
+			@entityHandler.copyFileFromExistingProjectWithProject
+				.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @project._id, @rootFolder.fileRefs[0], @owner._id)
+				.should.equal true
+			@entityHandler.copyFileFromExistingProjectWithProject
+				.calledWith(@stubbedNewProject, @newFolder._id, @project._id, @level1folder.fileRefs[0], @owner._id)
+				.should.equal true
+			@entityHandler.copyFileFromExistingProjectWithProject
+				.calledWith(@stubbedNewProject, @newFolder._id, @project._id, @level2folder.fileRefs[0], @owner._id)
+				.should.equal true
 			done()

+ 90 - 38
services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee

@@ -17,9 +17,10 @@ describe 'ProjectEntityHandler', ->
 	userId = 1234
 
 	beforeEach ->
+		@fileUrl = 'filestore.example.com/file'
 		@FileStoreHandler =
-			uploadFileFromDisk:(project_id, fileRef,  localImagePath, callback)->callback()
-			copyFile: sinon.stub().callsArgWith(4, null)
+			uploadFileFromDisk: sinon.stub().callsArgWith(3, null, @fileUrl)
+			copyFile: sinon.stub().callsArgWith(4, null, @fileUrl)
 		@tpdsUpdateSender =
 			addDoc:sinon.stub().callsArg(1)
 			addFile:sinon.stub().callsArg(1)
@@ -67,6 +68,9 @@ describe 'ProjectEntityHandler', ->
 			findElement : sinon.stub()
 		@settings =
 			maxEntitiesPerProject:200
+		@documentUpdaterHandler =
+			updateProjectStructure: sinon.stub().yields()
+			deleteDoc: sinon.stub().callsArg(2)
 		@ProjectEntityHandler = SandboxedModule.require modulePath, requires:
 			'../../models/Project': Project:@ProjectModel
 			'../../models/Doc': Doc:@DocModel
@@ -75,7 +79,7 @@ describe 'ProjectEntityHandler', ->
 			'../FileStore/FileStoreHandler':@FileStoreHandler
 			'../ThirdPartyDataStore/TpdsUpdateSender':@tpdsUpdateSender
 			'./ProjectLocator': @projectLocator
-			'../../Features/DocumentUpdater/DocumentUpdaterHandler':@documentUpdaterHandler = {}
+			'../../Features/DocumentUpdater/DocumentUpdaterHandler':@documentUpdaterHandler
 			'../Docstore/DocstoreManager': @DocstoreManager = {}
 			'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->}
 			'./ProjectUpdateHandler': @projectUpdater
@@ -184,7 +188,6 @@ describe 'ProjectEntityHandler', ->
 	describe "_cleanUpEntity", ->
 		beforeEach ->
 			@entity_id = "4eecaffcbffa66588e000009"
-			@documentUpdaterHandler.deleteDoc = sinon.stub().callsArg(2)
 			@FileStoreHandler.deleteFile = sinon.stub().callsArg(2)
 			@ProjectEntityHandler.unsetRootDoc = sinon.stub().callsArg(1)
 
@@ -240,7 +243,6 @@ describe 'ProjectEntityHandler', ->
 			@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, path: @pathAfterMove)
 			@ProjectGetter.getProject.callsArgWith(2, null, @project)
 			@tpdsUpdateSender.moveEntity = sinon.stub()
-			@documentUpdaterHandler.updateProjectStructure = sinon.stub().callsArg(6)
 			@ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub()
 			@ProjectEntityHandler.getAllEntitiesFromProject
 				.onFirstCall()
@@ -272,7 +274,7 @@ describe 'ProjectEntityHandler', ->
 
 			it "should should send the update to the doc updater", ->
 				@documentUpdaterHandler.updateProjectStructure
-					.calledWith(project_id, userId, @oldDocs, @newDocs, @oldFiles, @newFiles)
+					.calledWith(project_id, userId, {@oldDocs, @newDocs, @oldFiles, @newFiles})
 					.should.equal true
 
 			it 'should remove the element from its current position', ->
@@ -324,7 +326,7 @@ describe 'ProjectEntityHandler', ->
 
 				it "should should send the update to the doc updater", ->
 					@documentUpdaterHandler.updateProjectStructure
-						.calledWith(project_id, userId, @oldDocs, @newDocs, @oldFiles, @newFiles)
+						.calledWith(project_id, userId, {@oldDocs, @newDocs, @oldFiles, @newFiles})
 						.should.equal true
 
 				it 'should remove the element from its current position', ->
@@ -455,7 +457,7 @@ describe 'ProjectEntityHandler', ->
 			@tpdsUpdateSender.addDoc = sinon.stub().callsArg(1)
 			@DocstoreManager.updateDoc = sinon.stub().yields(null, true, 0)
 
-			@ProjectEntityHandler.addDoc project_id, folder_id, @name, @lines, @callback
+			@ProjectEntityHandler.addDoc project_id, folder_id, @name, @lines, userId, @callback
 
 			# Created doc
 			@doc = @ProjectEntityHandler._putElement.args[0][2]
@@ -484,6 +486,16 @@ describe 'ProjectEntityHandler', ->
 				.calledWith(project_id, @doc._id.toString(), @lines)
 				.should.equal true
 
+		it "should should send the change in project structure to the doc updater", () ->
+			newDocs = [
+				doc: @doc
+				path: @path
+				docLines: @lines.join('\n')
+			]
+			@documentUpdaterHandler.updateProjectStructure
+				.calledWith(project_id, userId, {newDocs})
+				.should.equal true
+
 	describe "restoreDoc", ->
 		beforeEach ->
 			@name = "doc-name"
@@ -512,7 +524,10 @@ describe 'ProjectEntityHandler', ->
 	describe 'addFile', ->
 		fileName = "something.jpg"
 		beforeEach ->
+			@fileSystemPath = "somehintg"
+			@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem: @fileSystemPath}})
 			@filePath = "somewhere"
+
 		it 'should upload it via the FileStoreHandler', (done)->
 			@FileStoreHandler.uploadFileFromDisk = (passedProject_id, file_id, filePath, callback)=>
 				file_id.should.equal "file_id"
@@ -520,7 +535,7 @@ describe 'ProjectEntityHandler', ->
 				filePath.should.equal @filePath
 				done()
 
-			@ProjectEntityHandler.addFile project_id, folder_id, fileName, @filePath, (err, fileRef, parentFolder)->
+			@ProjectEntityHandler.addFile project_id, folder_id, fileName, @filePath, userId, (err, fileRef, parentFolder)->
 
 		it 'should put file into folder by calling put element', (done)->
 			@ProjectEntityHandler._putElement = (passedProject, passedFolder_id, passedFileRef, passedType, callback)->
@@ -530,11 +545,10 @@ describe 'ProjectEntityHandler', ->
 				passedType.should.equal 'file'
 				done()
 
-			@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, (err, fileRef, parentFolder)->
+			@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, (err, fileRef, parentFolder)->
 
 		it 'should return doc and parent folder', (done)->
-			@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:"somehintg"}})
-			@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, (err, fileRef, parentFolder)->
+			@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, (err, fileRef, parentFolder)->
 				parentFolder.should.equal folder_id
 				fileRef.name.should.equal fileName
 				done()
@@ -554,33 +568,45 @@ describe 'ProjectEntityHandler', ->
 				options.rev.should.equal 0
 				done()
 
-			@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, (err, fileRef, parentFolder)->
+			@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, (err, fileRef, parentFolder)->
+
+		it "should should send the change in project structure to the doc updater", (done) ->
+			@documentUpdaterHandler.updateProjectStructure = (passed_project_id, passed_user_id, changes) =>
+				passed_project_id.should.equal project_id
+				passed_user_id.should.equal userId
+				{ newFiles } = changes
+				newFiles.length.should.equal 1
+				newFile = newFiles[0]
+				newFile.file.name.should.equal fileName
+				newFile.path.should.equal @fileSystemPath
+				newFile.url.should.equal @fileUrl
+				done()
 
-	describe 'replacing a file', ->
+			@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, () ->
 
+	describe 'replaceFile', ->
 		beforeEach ->
 			@projectLocator
 			@file_id = "file_id_here"
 			@fsPath = "fs_path_here.png"
-			@fileRef = {rev:3, _id:@file_id}
-			@filePaths = {fileSystem:"/folder1/file.png", mongo:"folder.1.files.somewhere"}
+			@fileRef = {rev:3, _id: @file_id, name: @fileName = "fileName"}
+			@filePaths = {fileSystem: @fileSystemPath="/folder1/file.png", mongo:"folder.1.files.somewhere"}
 			@projectLocator.findElement = sinon.stub().callsArgWith(1, null, @fileRef, @filePaths)
-			@ProjectModel.update = (_, __, ___, cb)-> cb()
+			@ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3)
 			@ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project)
 
 		it 'should find the file', (done)->
-
-			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, =>
-				@projectLocator.findElement.calledWith({element_id:@file_id, type:"file", project_id:project_id}).should.equal true
+			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
+				@projectLocator.findElement
+					.calledWith({element_id:@file_id, type:"file", project: @project})
+					.should.equal true
 				done()
 
 		it 'should tell the file store handler to upload the file from disk', (done)->
-			@FileStoreHandler.uploadFileFromDisk = sinon.stub().callsArgWith(3)
-			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, =>
+			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
 				@FileStoreHandler.uploadFileFromDisk.calledWith(project_id, @file_id, @fsPath).should.equal true
 				done()
 
-
 		it 'should send the file to the tpds with an incremented rev', (done)->
 			@tpdsUpdateSender.addFile = (options)=>
 				options.project_id.should.equal project_id
@@ -590,26 +616,39 @@ describe 'ProjectEntityHandler', ->
 				options.rev.should.equal @fileRef.rev + 1
 				done()
 
-			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, =>
+			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
 
 		it 'should inc the rev id', (done)->
-			@ProjectModel.update = (conditions, update, options, callback)=>
+			@ProjectModel.findOneAndUpdate = (conditions, update, options, callback)=>
 				conditions._id.should.equal project_id
 				update.$inc["#{@filePaths.mongo}.rev"].should.equal 1
 				done()
 
-			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, =>
+			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
 
 		it 'should update the created at date', (done)->
 			d = new Date()
-			@ProjectModel.update = (conditions, update, options, callback)=>
+			@ProjectModel.findOneAndUpdate = (conditions, update, options, callback)=>
 				conditions._id.should.equal project_id
 				differenceInMs = update.$set["#{@filePaths.mongo}.created"].getTime() - d.getTime()
 				differenceInMs.should.be.below(20)
 				done()
 
-			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, =>
+			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
+
+		it "should should send the old and new project structure to the doc updater", (done) ->
+			@documentUpdaterHandler.updateProjectStructure = (passed_project_id, passed_user_id, changes) =>
+				passed_project_id.should.equal project_id
+				passed_user_id.should.equal userId
+				{ newFiles } = changes
+				newFiles.length.should.equal 1
+				newFile = newFiles[0]
+				newFile.file.name.should.equal @fileName
+				newFile.path.should.equal @fileSystemPath
+				newFile.url.should.equal @fileUrl
+				done()
 
+			@ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
 
 	describe 'addFolder', ->
 		folderName = "folder1234"
@@ -943,19 +982,19 @@ describe 'ProjectEntityHandler', ->
 			@ProjectModel.update.calledWith({_id : @project_id}, {$unset : {rootDoc_id: true}})
 				.should.equal true
 
-	describe 'copyFileFromExistingProject', ->
+	describe 'copyFileFromExistingProjectWithProject', ->
 		fileName = "something.jpg"
 		filePath = "dumpFolder/somewhere/image.jpeg"
 		oldProject_id = "123kljadas"
 		oldFileRef = {name:fileName, _id:"oldFileRef"}
-		beforeEach ->
-			@ProjectGetter.getProject = (project_id, fields, callback)=> callback(null, {name:@project.name, _id:@project._id})
-			@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:"somehintg"}})
 
+		beforeEach ->
+			@fileSystemPath = "somehintg"
+			@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem: @fileSystemPath}})
 
 		it 'should copy the file in FileStoreHandler', (done)->
 			@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:"somehintg"}})
-			@ProjectEntityHandler.copyFileFromExistingProject project_id, folder_id, oldProject_id, oldFileRef, (err, fileRef, parentFolder)=>
+			@ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)=>
 				@FileStoreHandler.copyFile.calledWith(oldProject_id, oldFileRef._id, project_id, fileRef._id).should.equal true
 				done()
 
@@ -967,10 +1006,10 @@ describe 'ProjectEntityHandler', ->
 				passedType.should.equal 'file'
 				done()
 
-			@ProjectEntityHandler.copyFileFromExistingProject project_id, folder_id, oldProject_id, oldFileRef, (err, fileRef, parentFolder)->
+			@ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)->
 
 		it 'should return doc and parent folder', (done)->
-			@ProjectEntityHandler.copyFileFromExistingProject project_id, folder_id, oldProject_id, oldFileRef, (err, fileRef, parentFolder)->
+			@ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)->
 				parentFolder.should.equal folder_id
 				fileRef.name.should.equal fileName
 				done()
@@ -990,8 +1029,21 @@ describe 'ProjectEntityHandler', ->
 				options.rev.should.equal 0
 				done()
 
-			@ProjectEntityHandler.copyFileFromExistingProject project_id, folder_id, oldProject_id, oldFileRef, (err, fileRef, parentFolder)->
+			@ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)->
+
+		it "should should send the change in project structure to the doc updater", (done) ->
+			@documentUpdaterHandler.updateProjectStructure = (passed_project_id, passed_user_id, changes) =>
+				passed_project_id.should.equal project_id
+				passed_user_id.should.equal userId
+				{ newFiles } = changes
+				newFiles.length.should.equal 1
+				newFile = newFiles[0]
+				newFile.file.name.should.equal fileName
+				newFile.path.should.equal @fileSystemPath
+				newFile.url.should.equal @fileUrl
+				done()
 
+			@ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)->
 
 	describe "renameEntity", ->
 		beforeEach ->
@@ -1012,12 +1064,12 @@ describe 'ProjectEntityHandler', ->
 			@projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: @entity_id, name:"old.tex", rev:4 }, @path)
 			@tpdsUpdateSender.moveEntity = sinon.stub()
 			@ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3, null, @project)
-			@documentUpdaterHandler.updateProjectStructure = sinon.stub().callsArg(6)
+			@documentUpdaterHandler.updateProjectStructure = sinon.stub().yields()
 
 		it "should should send the old and new project structure to the doc updater", (done) ->
 			@ProjectEntityHandler.renameEntity project_id, @entity_id, @entityType, @newName, userId, =>
 				@documentUpdaterHandler.updateProjectStructure
-					.calledWith(project_id, userId, @oldDocs, @newDocs, @oldFiles, @newFiles)
+					.calledWith(project_id, userId, {@oldDocs, @newDocs, @oldFiles, @newFiles})
 					.should.equal true
 				done()
 

+ 9 - 9
services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee

@@ -63,11 +63,11 @@ describe 'UpdateMerger :', ->
 			file_id = "1231"
 			@projectLocator.findElementByPath = (_, __, cb)->cb(null, {_id:file_id})
 			@FileTypeManager.isBinary.callsArgWith(2, null, true)
-			@updateMerger.p.processFile = sinon.stub().callsArgWith(5)
+			@updateMerger.p.processFile = sinon.stub().callsArgWith(6)
 			filePath = "/folder/file1.png"
 
 			@updateMerger.mergeUpdate @user_id, @project_id, filePath, @update, @source, =>
-				@updateMerger.p.processFile.calledWith(@project_id, file_id, @fsPath, filePath, @source).should.equal true
+				@updateMerger.p.processFile.calledWith(@project_id, file_id, @fsPath, filePath, @source, @user_id).should.equal true
 				@FileTypeManager.isBinary.calledWith(filePath, @fsPath).should.equal true
 				@fs.unlink.calledWith(@fsPath).should.equal true
 				done()
@@ -97,7 +97,7 @@ describe 'UpdateMerger :', ->
 			path = "folder1/folder2/#{docName}"
 			@editorController.mkdirp = sinon.stub().withArgs(@project_id).callsArgWith(2, null, [folder], folder)
 			@editorController.addDoc = ->
-			mock = sinon.mock(@editorController).expects("addDoc").withArgs(@project_id, folder._id, docName, @splitDocLines, @source).callsArg(5)
+			mock = sinon.mock(@editorController).expects("addDoc").withArgs(@project_id, folder._id, docName, @splitDocLines, @source, @user_id).callsArg(6)
 
 			@update.write(@docLines)
 			@update.end()
@@ -114,22 +114,22 @@ describe 'UpdateMerger :', ->
 			@folder = _id: @folder_id
 			@fileName = "file.png"
 			@fsPath = "fs/path.tex"
-			@editorController.addFile = sinon.stub().callsArg(5)
-			@editorController.replaceFile = sinon.stub().callsArg(4)
+			@editorController.addFile = sinon.stub().callsArg(6)
+			@editorController.replaceFile = sinon.stub().callsArg(5)
 			@editorController.deleteEntity = sinon.stub()
 			@editorController.mkdirp = sinon.stub().withArgs(@project_id).callsArgWith(2, null, [@folder], @folder)
 			@updateMerger.p.writeStreamToDisk = sinon.stub().withArgs(@project_id, @file_id, @update).callsArgWith(3, null, @fsPath)
 
 		it 'should replace file if the file already exists', (done)->
-			@updateMerger.p.processFile @project_id, @file_id, @fsPath, @path, @source, =>
+			@updateMerger.p.processFile @project_id, @file_id, @fsPath, @path, @source, @user_id, =>
 				@editorController.addFile.called.should.equal false
-				@editorController.replaceFile.calledWith(@project_id, @file_id, @fsPath, @source).should.equal true
+				@editorController.replaceFile.calledWith(@project_id, @file_id, @fsPath, @source, @user_id).should.equal true
 				done()
 
 		it 'should call add file if the file does not exist', (done)->
-			@updateMerger.p.processFile @project_id, undefined, @fsPath, @path, @source, =>
+			@updateMerger.p.processFile @project_id, undefined, @fsPath, @path, @source, @user_id, =>
 				@editorController.mkdirp.calledWith(@project_id, "folder/").should.equal true
-				@editorController.addFile.calledWith(@project_id, @folder_id, @fileName, @fsPath, @source).should.equal true
+				@editorController.addFile.calledWith(@project_id, @folder_id, @fileName, @fsPath, @source, @user_id).should.equal true
 				@editorController.replaceFile.called.should.equal false
 				done()
 

+ 12 - 12
services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee

@@ -44,14 +44,14 @@ describe "FileSystemImportManager", ->
 
 		describe "with replace set to false", ->
 			beforeEach ->
-				@EditorController.addDocWithoutLock = sinon.stub().callsArg(5)
+				@EditorController.addDocWithoutLock = sinon.stub().callsArg(6)
 				@FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback
 
 			it "should read the file from disk", ->
 				@fs.readFile.calledWith(@path_on_disk, "utf8").should.equal true
 
 			it "should insert the doc", ->
-				@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload")
+				@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id)
 					.should.equal true
 
 		describe "with windows line ending", ->
@@ -59,11 +59,11 @@ describe "FileSystemImportManager", ->
 				@docContent = "one\r\ntwo\r\nthree"
 				@docLines = ["one", "two", "three"]
 				@fs.readFile = sinon.stub().callsArgWith(2, null, @docContent)
-				@EditorController.addDocWithoutLock = sinon.stub().callsArg(5)
+				@EditorController.addDocWithoutLock = sinon.stub().callsArg(6)
 				@FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback
 
 			it "should strip the \\r characters before adding", ->
-				@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload")
+				@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id)
 					.should.equal true
 		
 		describe "with replace set to true", ->
@@ -76,7 +76,7 @@ describe "FileSystemImportManager", ->
 						}]
 					}
 					@ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder)
-					@EditorController.addDocWithoutLock = sinon.stub().callsArg(5)
+					@EditorController.addDocWithoutLock = sinon.stub().callsArg(6)
 					@FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback
 
 				it "should look up the folder", ->
@@ -85,7 +85,7 @@ describe "FileSystemImportManager", ->
 						.should.equal true
 
 				it "should insert the doc", ->
-					@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload")
+					@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id)
 						.should.equal true
 
 			describe "when the doc does exist", ->
@@ -114,12 +114,12 @@ describe "FileSystemImportManager", ->
 
 	describe "addFile with replace set to false", ->
 		beforeEach ->
-			@EditorController.addFileWithoutLock = sinon.stub().callsArg(5)
+			@EditorController.addFileWithoutLock = sinon.stub().callsArg(6)
 			@FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true)
 			@FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback
 
 		it "should add the file", ->
-			@EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload")
+			@EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload", @user_id)
 				.should.equal true
 
 	describe "addFile with symlink", ->
@@ -144,7 +144,7 @@ describe "FileSystemImportManager", ->
 				}
 				@FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true)
 				@ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder)
-				@EditorController.addFileWithoutLock = sinon.stub().callsArg(5)
+				@EditorController.addFileWithoutLock = sinon.stub().callsArg(6)
 				@FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback
 
 			it "should look up the folder", ->
@@ -153,7 +153,7 @@ describe "FileSystemImportManager", ->
 					.should.equal true
 
 			it "should add the file", ->
-				@EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload")
+				@EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload", @user_id)
 					.should.equal true
 
 		describe "when the file does exist", ->
@@ -169,7 +169,7 @@ describe "FileSystemImportManager", ->
 				}
 				@FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true)
 				@ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder)
-				@EditorController.replaceFile = sinon.stub().callsArg(4)
+				@EditorController.replaceFile = sinon.stub().callsArg(5)
 				@FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback
 
 			it "should look up the folder", ->
@@ -178,7 +178,7 @@ describe "FileSystemImportManager", ->
 					.should.equal true
 
 			it "should replace the file", ->
-				@EditorController.replaceFile.calledWith(@project_id, @file_id, @path_on_disk, "upload")
+				@EditorController.replaceFile.calledWith(@project_id, @file_id, @path_on_disk, "upload", @user_id)
 					.should.equal true
 
 	describe "addFolder", ->