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

Merge pull request #377 from sharelatex/bg-add-project-structure-version-number

add project structure version number
Brian Gough 8 лет назад
Родитель
Сommit
3bbd49c7eb

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

@@ -205,29 +205,32 @@ module.exports = DocumentUpdaterHandler =
 
 	updateProjectStructure : (project_id, userId, changes, callback = (error) ->)->
 		return callback() if !settings.apis.project_history?.sendProjectStructureOps
+		Project.findOne {_id: project_id}, {version:true}, (err, currentProject) ->
+			return callback(err) if err?
+			return callback new Error("project not found") if !currentProject?
 
-		docUpdates = DocumentUpdaterHandler._getUpdates('doc', changes.oldDocs, changes.newDocs)
-		fileUpdates = DocumentUpdaterHandler._getUpdates('file', changes.oldFiles, changes.newFiles)
+			docUpdates = DocumentUpdaterHandler._getUpdates('doc', changes.oldDocs, changes.newDocs)
+			fileUpdates = DocumentUpdaterHandler._getUpdates('file', changes.oldFiles, changes.newFiles)
 
-		timer = new metrics.Timer("set-document")
-		url = "#{settings.apis.documentupdater.url}/project/#{project_id}"
-		body =
-			url: url
-			json: { docUpdates, fileUpdates, userId }
+			timer = new metrics.Timer("set-document")
+			url = "#{settings.apis.documentupdater.url}/project/#{project_id}"
+			body =
+				url: url
+				json: { docUpdates, fileUpdates, userId, version: currentProject.version }
 
-		return callback() if (docUpdates.length + fileUpdates.length) < 1
+			return callback() if (docUpdates.length + fileUpdates.length) < 1
 
-		request.post body, (error, res, body)->
-			timer.done()
-			if error?
-				logger.error {error, url, project_id}, "error update project structure in doc updater"
-				callback(error)
-			else if res.statusCode >= 200 and res.statusCode < 300
-				logger.log {project_id}, "updated project structure in doc updater"
-				callback(null)
-			else
-				logger.error {project_id, url}, "doc updater returned a non-success status code: #{res.statusCode}"
-				callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
+			request.post body, (error, res, body)->
+				timer.done()
+				if error?
+					logger.error {error, url, project_id}, "error update project structure in doc updater"
+					callback(error)
+				else if res.statusCode >= 200 and res.statusCode < 300
+					logger.log {project_id}, "updated project structure in doc updater"
+					callback(null)
+				else
+					logger.error {project_id, url}, "doc updater returned a non-success status code: #{res.statusCode}"
+					callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
 
 	_getUpdates: (entityType, oldEntities, newEntities) ->
 		oldEntities ||= []

+ 16 - 5
services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee

@@ -56,6 +56,11 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
 				conditions = _id:project._id
 				inc = {}
 				inc["#{path.mongo}.rev"] = 1
+				# currently we do not need to increment the project version number for changes that are replacements
+				# but when we make switch to having immutable files the replace operation will add a new file, and
+				# this will require a version increase.  We will start incrementing the project version now as it does
+				# no harm and will help to test it.
+				inc['version'] = 1
 				set = {}
 				set["#{path.mongo}.created"] = new Date()
 				update =
@@ -144,9 +149,11 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
 						return callback(error) if error?
 						endPath = path.join(path.dirname(entPath.fileSystem), newName)
 						conditions = {_id:project_id}
-						update = "$set":{}
+						update = "$set":{}, "$inc":{}
 						namePath = entPath.mongo+".name"
 						update["$set"][namePath] = newName
+						# we need to increment the project version number for any structure change
+						update["$inc"]["version"] = 1
 						Project.findOneAndUpdate conditions, update, { "new": true}, (error, newProject) ->
 							return callback(error) if error?
 							ProjectEntityHandler.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) =>
@@ -174,9 +181,11 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
 		update = {"$unset":{}}
 		update["$unset"][path] = 1
 		model.update conditions, update, {}, (err)->
-			pullUpdate = {"$pull":{}}
+			pullUpdate = {"$pull":{}, "$inc":{}}
 			nonArrayPath = path.slice(0, path.lastIndexOf("."))
 			pullUpdate["$pull"][nonArrayPath] = null
+			# we need to increment the project version number for any structure change
+			pullUpdate["$inc"]["version"] = 1
 			model.findOneAndUpdate conditions, pullUpdate, {"new": true}, callback
 
 	_countElements: (project)->
@@ -245,14 +254,16 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
 				element._id = require('mongoose').Types.ObjectId(id)
 				conditions = _id:project._id
 				mongopath = "#{path.mongo}.#{type}"
-				update = "$push":{}
+				update = "$push":{}, "$inc":{}
 				update["$push"][mongopath] = element
+				# we need to increment the project version number for any structure change
+				update["$inc"]["version"] = 1 # increment project version number
 				logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, mongopath:mongopath, "adding element to project"
-				Project.findOneAndUpdate conditions, update, {"new": true}, (err, project)->
+				Project.findOneAndUpdate conditions, update, {"new": true}, (err, newProject)->
 					if err?
 						logger.err err: err, project_id: project._id, 'error saving in putElement project'
 						return callback(err)
-					callback(err, {path:newPath}, project)
+					callback(err, {path:newPath}, newProject)
 
 	_checkValidElementName: (folder, name, callback = (err) ->) ->
 		# check if the name is already taken by a doc, file or

+ 1 - 0
services/web/app/coffee/models/Project.coffee

@@ -22,6 +22,7 @@ ProjectSchema = new Schema
 	readOnly_refs     :   [ type:ObjectId, ref:'User' ]
 	rootDoc_id        :   {type: ObjectId}
 	rootFolder        :   [FolderSchema]
+	version           :   {type: Number} # incremented for every change in the project structure (folders and filenames)
 	publicAccesLevel  :   {type: String, default: 'private'}
 	compiler          :   {type:String, default:'pdflatex'}
 	spellCheckLanguage :   {type:String, default:'en'}

+ 162 - 47
services/web/test/acceptance/coffee/ProjectStructureTests.coffee

@@ -35,21 +35,23 @@ describe "ProjectStructureChanges", ->
 				done()
 
 		it "should version creating a doc", ->
-			updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).docUpdates
+			{docUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 			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
+			expect(version).to.equal(3)
 
 		it "should version creating a file", ->
-			updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).fileUpdates
+			{fileUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 			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');
+			expect(version).to.equal(3)
 
 	describe "duplicating a project", ->
 		before (done) ->
@@ -67,21 +69,23 @@ describe "ProjectStructureChanges", ->
 				done()
 
 		it "should version the docs created", ->
-			updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).docUpdates
+			{docUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id)
 			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)
+			expect(version).to.equal(3)
 
 		it "should version the files created", ->
-			updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).fileUpdates
+			{fileUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id)
 			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');
+			expect(version).to.equal(3)
 
 	describe "adding a doc", ->
 		before (done) ->
@@ -89,6 +93,7 @@ describe "ProjectStructureChanges", ->
 
 			ProjectGetter.getProject example_project_id, (error, project) =>
 				throw error if error?
+				@project_0 = project
 				@owner.request.post {
 					uri: "project/#{example_project_id}/doc",
 					json:
@@ -99,15 +104,22 @@ describe "ProjectStructureChanges", ->
 					if res.statusCode < 200 || res.statusCode >= 300
 						throw new Error("failed to add doc #{res.statusCode}")
 					example_doc_id = body._id
-					done()
+					ProjectGetter.getProject example_project_id, (error, newProject) =>
+						throw error if error?
+						@project_1 = newProject
+						done()
 
 		it "should version the doc added", ->
-			updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).docUpdates
+			{docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 			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');
+			expect(version).to.equal(@project_0.version + 1)
+
+		it "should increment the project structure version number", ->
+			expect(@project_1.version).to.equal(@project_0.version + 1)
 
 	describe "uploading a project", ->
 		before (done) ->
@@ -127,31 +139,32 @@ describe "ProjectStructureChanges", ->
 				done()
 
 		it "should version the dosc created", ->
-			updates = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id).docUpdates
+			{docUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id)
 			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")
+			expect(version).to.equal(2)
 
 		it "should version the files created", ->
-			updates = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id).fileUpdates
+			{fileUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id)
 			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');
+			expect(version).to.equal(2)
 
 	describe "uploading a file", ->
-		before (done) ->
+		beforeEach (done) ->
+			MockDocUpdaterApi.clearProjectStructureUpdates()
 			ProjectGetter.getProject example_project_id, (error, project) =>
 				throw error if error?
 				@root_folder_id = project.rootFolder[0]._id.toString()
+				@project_0 = project
 				done()
-
-		beforeEach () ->
-			MockDocUpdaterApi.clearProjectStructureUpdates()
-
+				
 		it "should version a newly uploaded file", (done) ->
 			image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/1pixel.png'))
 
@@ -172,15 +185,21 @@ describe "ProjectStructureChanges", ->
 
 				example_file_id = JSON.parse(body).entity_id
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).fileUpdates
+				{fileUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 				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
+				expect(version).to.equal(@project_0.version + 1)
 
-				done()
+				ProjectGetter.getProject example_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# uploading a new file does change the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 1)
+					done()
 
 		it "should version a replacement file", (done) ->
 			image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/2pixel.png'))
@@ -200,14 +219,20 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to upload file #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).fileUpdates
+				{fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 				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');
+				expect(version).to.equal(@project_0.version + 1)
 
-				done()
+				ProjectGetter.getProject example_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 1)
+					done()
 
 	describe "moving entities", ->
 		before (done) ->
@@ -220,8 +245,13 @@ describe "ProjectStructureChanges", ->
 				example_folder_id_1 = JSON.parse(body)._id
 				done()
 
-		beforeEach () ->
+		beforeEach (done) ->
 			MockDocUpdaterApi.clearProjectStructureUpdates()
+			ProjectGetter.getProject example_project_id, (error, project) =>
+				throw error if error?
+				@root_folder_id = project.rootFolder[0]._id.toString()
+				@project_0 = project
+				done()
 
 		it "should version moving a doc", (done) ->
 			@owner.request.post {
@@ -233,14 +263,20 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to move doc #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).docUpdates
+				{docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 				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.newPathname).to.equal("/foo/new.tex")
+				expect(version).to.equal(@project_0.version + 2)
 
-				done()
+				ProjectGetter.getProject example_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 2)  # 2 because it's a delete and then add
+					done()
 
 		it "should version moving a file", (done) ->
 			@owner.request.post {
@@ -252,14 +288,20 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to move file #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).fileUpdates
+				{fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 				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.newPathname).to.equal("/foo/1pixel.png")
+				expect(version).to.equal(@project_0.version + 2)
 
-				done()
+				ProjectGetter.getProject example_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 2) # 2 because it's a delete and then add
+					done()
 
 		it "should version moving a folder", (done) ->
 			@owner.request.post {
@@ -279,25 +321,37 @@ describe "ProjectStructureChanges", ->
 					if res.statusCode < 200 || res.statusCode >= 300
 						throw new Error("failed to move folder #{res.statusCode}")
 
-					updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).docUpdates
+					{docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 					expect(updates.length).to.equal(1)
 					update = updates[0]
 					expect(update.userId).to.equal(@owner._id)
 					expect(update.pathname).to.equal("/foo/new.tex")
 					expect(update.newPathname).to.equal("/bar/foo/new.tex")
+					expect(version).to.equal(@project_0.version + 3)
 
-					updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).fileUpdates
+					{fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 					expect(updates.length).to.equal(1)
 					update = updates[0]
 					expect(update.userId).to.equal(@owner._id)
 					expect(update.pathname).to.equal("/foo/1pixel.png")
 					expect(update.newPathname).to.equal("/bar/foo/1pixel.png")
+					expect(version).to.equal(@project_0.version + 3)
 
-					done()
+					ProjectGetter.getProject example_project_id, (error, newProject) =>
+						throw error if error?
+						@project_1 = newProject
+						# replacing a file should update the project structure
+						expect(@project_1.version).to.equal(@project_0.version + 3) # because folder and 2 files move
+						done()
 
 	describe "renaming entities", ->
-		beforeEach () ->
+		beforeEach (done) ->
 			MockDocUpdaterApi.clearProjectStructureUpdates()
+			ProjectGetter.getProject example_project_id, (error, project) =>
+				throw error if error?
+				@root_folder_id = project.rootFolder[0]._id.toString()
+				@project_0 = project
+				done()
 
 		it "should version renaming a doc", (done) ->
 			@owner.request.post {
@@ -309,14 +363,20 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to move doc #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).docUpdates
+				{docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 				expect(updates.length).to.equal(1)
 				update = updates[0]
 				expect(update.userId).to.equal(@owner._id)
 				expect(update.pathname).to.equal("/bar/foo/new.tex")
 				expect(update.newPathname).to.equal("/bar/foo/new_renamed.tex")
+				expect(version).to.equal(@project_0.version + 1)
 
-				done()
+				ProjectGetter.getProject example_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 1)
+					done()
 
 		it "should version renaming a file", (done) ->
 			@owner.request.post {
@@ -328,14 +388,20 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to move file #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).fileUpdates
+				{fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 				expect(updates.length).to.equal(1)
 				update = updates[0]
 				expect(update.userId).to.equal(@owner._id)
 				expect(update.pathname).to.equal("/bar/foo/1pixel.png")
 				expect(update.newPathname).to.equal("/bar/foo/1pixel_renamed.png")
+				expect(version).to.equal(@project_0.version + 1)
 
-				done()
+				ProjectGetter.getProject example_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 1)
+					done()
 
 		it "should version renaming a folder", (done) ->
 			@owner.request.post {
@@ -347,25 +413,38 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to move folder #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).docUpdates
+				{docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 				expect(updates.length).to.equal(1)
 				update = updates[0]
 				expect(update.userId).to.equal(@owner._id)
 				expect(update.pathname).to.equal("/bar/foo/new_renamed.tex")
 				expect(update.newPathname).to.equal("/bar/foo_renamed/new_renamed.tex")
+				expect(version).to.equal(@project_0.version + 1)
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).fileUpdates
+				{fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 				expect(updates.length).to.equal(1)
 				update = updates[0]
 				expect(update.userId).to.equal(@owner._id)
 				expect(update.pathname).to.equal("/bar/foo/1pixel_renamed.png")
 				expect(update.newPathname).to.equal("/bar/foo_renamed/1pixel_renamed.png")
+				expect(version).to.equal(@project_0.version + 1)
+
+				ProjectGetter.getProject example_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 1)
+					done()
 
-				done()
 
 	describe "deleting entities", ->
-		beforeEach () ->
+		beforeEach (done) ->
 			MockDocUpdaterApi.clearProjectStructureUpdates()
+			ProjectGetter.getProject example_project_id, (error, project) =>
+				throw error if error?
+				@root_folder_id = project.rootFolder[0]._id.toString()
+				@project_0 = project
+				done()
 
 		it "should version deleting a folder", (done) ->
 			@owner.request.delete {
@@ -375,21 +454,28 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to delete folder #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).docUpdates
+				{docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 				expect(updates.length).to.equal(1)
 				update = updates[0]
 				expect(update.userId).to.equal(@owner._id)
 				expect(update.pathname).to.equal("/bar/foo_renamed/new_renamed.tex")
 				expect(update.newPathname).to.equal("")
+				expect(version).to.equal(@project_0.version + 1)
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id).fileUpdates
+				{fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)
 				expect(updates.length).to.equal(1)
 				update = updates[0]
 				expect(update.userId).to.equal(@owner._id)
 				expect(update.pathname).to.equal("/bar/foo_renamed/1pixel_renamed.png")
 				expect(update.newPathname).to.equal("")
+				expect(version).to.equal(@project_0.version + 1)
 
-				done()
+				ProjectGetter.getProject example_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 1)
+					done()
 
 	describe "tpds", ->
 		before (done) ->
@@ -399,8 +485,13 @@ describe "ProjectStructureChanges", ->
 				@tpds_project_id = project_id
 				mkdirp Settings.path.dumpFolder, done
 
-		beforeEach () ->
+		beforeEach (done) ->
 			MockDocUpdaterApi.clearProjectStructureUpdates()
+			ProjectGetter.getProject @tpds_project_id, (error, project) =>
+				throw error if error?
+				@root_folder_id = project.rootFolder[0]._id.toString()
+				@project_0 = project
+				done()
 
 		it "should version adding a doc", (done) ->
 			tex_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test.tex'))
@@ -423,14 +514,20 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to upload file #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).docUpdates
+				{docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id)
 				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")
+				expect(version).to.equal(@project_0.version + 1)
 
-				done()
+				ProjectGetter.getProject @tpds_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 1)
+					done()
 
 			tex_file.pipe(req)
 
@@ -455,14 +552,20 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to upload file #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).fileUpdates
+				{fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id)
 				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');
+				expect(version).to.equal(@project_0.version + 1)
 
-				done()
+				ProjectGetter.getProject @tpds_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 1)
+					done()
 
 			image_file.pipe(req)
 
@@ -487,14 +590,20 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to upload file #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).fileUpdates
+				{fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id)
 				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');
+				expect(version).to.equal(@project_0.version + 1)
 
-				done()
+				ProjectGetter.getProject @tpds_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 1)
+					done()
 
 			image_file.pipe(req)
 
@@ -510,12 +619,18 @@ describe "ProjectStructureChanges", ->
 				if res.statusCode < 200 || res.statusCode >= 300
 					throw new Error("failed to delete doc #{res.statusCode}")
 
-				updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).docUpdates
+				{docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id)
 				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.newPathname).to.equal("")
-
-				done()
+				expect(version).to.equal(@project_0.version + 1)
+				
+				ProjectGetter.getProject @tpds_project_id, (error, newProject) =>
+					throw error if error?
+					@project_1 = newProject
+					# replacing a file should update the project structure
+					expect(@project_1.version).to.equal(@project_0.version + 1)
+					done()
 

+ 5 - 3
services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee

@@ -12,7 +12,7 @@ module.exports = MockDocUpdaterApi =
 	getProjectStructureUpdates: (project_id) ->
 		@updates[project_id] || { docUpdates: [], fileUpdates: [] }
 
-	addProjectStructureUpdates: (project_id, userId, docUpdates, fileUpdates) ->
+	addProjectStructureUpdates: (project_id, userId, docUpdates, fileUpdates, version) ->
 		@updates[project_id] ||= { docUpdates: [], fileUpdates: [] }
 
 		for update in docUpdates
@@ -22,6 +22,8 @@ module.exports = MockDocUpdaterApi =
 		for update in fileUpdates
 			update.userId = userId
 			@updates[project_id].fileUpdates.push(update)
+		
+		@updates[project_id].version = version
 
 	run: () ->
 		app.post "/project/:project_id/flush", (req, res, next) =>
@@ -29,8 +31,8 @@ module.exports = MockDocUpdaterApi =
 
 		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)
+			{userId, docUpdates, fileUpdates, version} = req.body
+			@addProjectStructureUpdates(project_id, userId, docUpdates, fileUpdates, version)
 			res.sendStatus 200
 
 		app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>

+ 6 - 4
services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee

@@ -390,6 +390,8 @@ describe 'DocumentUpdaterHandler', ->
 	describe "updateProjectStructure ", ->
 		beforeEach ->
 			@user_id = 1234
+			@version = 999
+			@Project.findOne = sinon.stub().callsArgWith(2,null, {_id: @project_id, version:@version})
 
 		describe "with project history disabled", ->
 			beforeEach ->
@@ -434,7 +436,7 @@ describe 'DocumentUpdaterHandler', ->
 
 					@handler.updateProjectStructure @project_id, @user_id, @changes, () =>
 						@request.post
-							.calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id})
+							.calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id, version:@version})
 							.should.equal true
 						done()
 
@@ -454,7 +456,7 @@ describe 'DocumentUpdaterHandler', ->
 
 					@handler.updateProjectStructure @project_id, @user_id, @changes, () =>
 						@request.post
-							.calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id})
+							.calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id, version:@version})
 							.should.equal true
 						done()
 
@@ -474,7 +476,7 @@ describe 'DocumentUpdaterHandler', ->
 
 					@handler.updateProjectStructure @project_id, @user_id, @changes, () =>
 						@request.post
-							.calledWith(url: @url, json: {docUpdates: [], fileUpdates, userId: @user_id})
+							.calledWith(url: @url, json: {docUpdates: [], fileUpdates, userId: @user_id, version:@version})
 							.should.equal true
 						done()
 
@@ -493,7 +495,7 @@ describe 'DocumentUpdaterHandler', ->
 
 					@handler.updateProjectStructure @project_id, @user_id, @changes, () =>
 						@request.post
-							.calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id})
+							.calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id, version:@version})
 							.should.equal true
 						done()
 

+ 3 - 3
services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee

@@ -117,7 +117,7 @@ describe 'ProjectEntityMongoUpdateHandler', ->
 				.calledWith(
 					{ _id: project_id },
 					{
-						'$inc': { 'file.png.rev': 1 }
+						'$inc': { 'file.png.rev': 1, 'version': 1 }
 						'$set': { 'file.png.created': new Date() }
 					}
 					{}
@@ -324,7 +324,7 @@ describe 'ProjectEntityMongoUpdateHandler', ->
 			@ProjectModel.findOneAndUpdate
 				.calledWith(
 					{ _id: project_id },
-					{ $set: { "mongo.path.name": @newName } },
+					{ $set: { "mongo.path.name": @newName }, $inc: {"version": 1} },
 					{ new: true }
 				).should.equal true
 
@@ -384,7 +384,7 @@ describe 'ProjectEntityMongoUpdateHandler', ->
 
 		it 'should pull', ->
 			@ProjectModel.findOneAndUpdate
-				.calledWith({ _id: @id }, { '$pull': { 'folders[0]': null } }, {'new': true})
+				.calledWith({ _id: @id }, { '$pull': { 'folders[0]': null }, '$inc': {'version': 1} }, {'new': true})
 				.should.equal true
 
 		it 'should call the callback', ->