Przeglądaj źródła

Merge branch 'deploy-20181029a'

GitOrigin-RevId: c51d4da706319564bd31514370fe13413655b6cf
Douglas Lovell 8 lat temu
rodzic
commit
b1bda995a6

+ 2 - 13
services/web/app/coffee/Features/Compile/CompileManager.coffee

@@ -28,8 +28,8 @@ module.exports = CompileManager =
 			CompileManager._checkIfAutoCompileLimitHasBeenHit options.isAutoCompile, "everyone", (err, canCompile)->
 			CompileManager._checkIfAutoCompileLimitHasBeenHit options.isAutoCompile, "everyone", (err, canCompile)->
 				if !canCompile
 				if !canCompile
 					return callback null, "autocompile-backoff", []
 					return callback null, "autocompile-backoff", []
-				
-				CompileManager._ensureRootDocumentIsSet project_id, (error) ->
+
+				ProjectRootDocManager.ensureRootDocumentIsSet project_id, (error) ->
 					return callback(error) if error?
 					return callback(error) if error?
 					CompileManager.getProjectCompileLimits project_id, (error, limits) ->
 					CompileManager.getProjectCompileLimits project_id, (error, limits) ->
 						return callback(error) if error?
 						return callback(error) if error?
@@ -103,17 +103,6 @@ module.exports = CompileManager =
 				Metrics.inc "auto-compile-#{compileGroup}-limited"
 				Metrics.inc "auto-compile-#{compileGroup}-limited"
 			callback err, canCompile
 			callback err, canCompile
 
 
-	_ensureRootDocumentIsSet: (project_id, callback = (error) ->) ->
-		ProjectGetter.getProject project_id, rootDoc_id: 1, (error, project) ->
-			return callback(error) if error?
-			if !project?
-				return callback new Error("project not found")
-
-			if project.rootDoc_id?
-				callback()
-			else
-				ProjectRootDocManager.setRootDocAutomatically project_id, callback
-		
 	wordCount: (project_id, user_id, file, callback = (error) ->) ->
 	wordCount: (project_id, user_id, file, callback = (error) ->) ->
 		CompileManager.getProjectCompileLimits project_id, (error, limits) ->
 		CompileManager.getProjectCompileLimits project_id, (error, limits) ->
 			return callback(error) if error?
 			return callback(error) if error?

+ 12 - 6
services/web/app/coffee/Features/Exports/ExportsController.coffee

@@ -4,7 +4,7 @@ logger = require("logger-sharelatex")
 
 
 module.exports =
 module.exports =
 
 
-	exportProject: (req, res) ->
+	exportProject: (req, res, next) ->
 		{project_id, brand_variation_id} = req.params
 		{project_id, brand_variation_id} = req.params
 		user_id = AuthenticationController.getLoggedInUserId(req)
 		user_id = AuthenticationController.getLoggedInUserId(req)
 		export_params = {
 		export_params = {
@@ -21,10 +21,10 @@ module.exports =
 			export_params.description = req.body.description.trim() if req.body.description
 			export_params.description = req.body.description.trim() if req.body.description
 			export_params.author = req.body.author.trim() if req.body.author
 			export_params.author = req.body.author.trim() if req.body.author
 			export_params.license = req.body.license.trim() if req.body.license
 			export_params.license = req.body.license.trim() if req.body.license
-			export_params.show_source = req.body.show_source if req.body.show_source
+			export_params.show_source = req.body.showSource if req.body.showSource?
 
 
 		ExportsHandler.exportProject export_params, (err, export_data) ->
 		ExportsHandler.exportProject export_params, (err, export_data) ->
-			return err if err?
+			return next(err) if err?
 			logger.log
 			logger.log
 				user_id:user_id
 				user_id:user_id
 				project_id: project_id
 				project_id: project_id
@@ -36,7 +36,13 @@ module.exports =
 	exportStatus: (req, res) ->
 	exportStatus: (req, res) ->
 		{export_id} = req.params
 		{export_id} = req.params
 		ExportsHandler.fetchExport export_id, (err, export_json) ->
 		ExportsHandler.fetchExport export_id, (err, export_json) ->
-			return err if err?
+			if err?
+				json = {
+					status_summary: 'failed',
+					status_detail: err.toString,
+				}
+				res.send export_json: json
+				return err
 			parsed_export = JSON.parse(export_json)
 			parsed_export = JSON.parse(export_json)
 			json = {
 			json = {
 				status_summary: parsed_export.status_summary,
 				status_summary: parsed_export.status_summary,
@@ -46,11 +52,11 @@ module.exports =
 			}
 			}
 			res.send export_json: json
 			res.send export_json: json
 
 
-	exportDownload: (req, res) ->
+	exportDownload: (req, res, next) ->
 		{type, export_id} = req.params
 		{type, export_id} = req.params
 
 
 		AuthenticationController.getLoggedInUserId(req)
 		AuthenticationController.getLoggedInUserId(req)
 		ExportsHandler.fetchDownload export_id, type, (err, export_file_url) ->
 		ExportsHandler.fetchDownload export_id, type, (err, export_file_url) ->
-			return err if err?
+			return next(err) if err?
 
 
 			res.redirect export_file_url
 			res.redirect export_file_url

+ 7 - 3
services/web/app/coffee/Features/Exports/ExportsHandler.coffee

@@ -1,5 +1,6 @@
 ProjectGetter = require('../Project/ProjectGetter')
 ProjectGetter = require('../Project/ProjectGetter')
 ProjectLocator = require('../Project/ProjectLocator')
 ProjectLocator = require('../Project/ProjectLocator')
+ProjectRootDocManager = require('../Project/ProjectRootDocManager')
 UserGetter = require('../User/UserGetter')
 UserGetter = require('../User/UserGetter')
 logger = require('logger-sharelatex')
 logger = require('logger-sharelatex')
 settings = require 'settings-sharelatex'
 settings = require 'settings-sharelatex'
@@ -20,13 +21,16 @@ module.exports = ExportsHandler = self =
 				callback null, export_data
 				callback null, export_data
 
 
 	_buildExport: (export_params, callback=(err, export_data) ->) ->
 	_buildExport: (export_params, callback=(err, export_data) ->) ->
-		{project_id, user_id, brand_variation_id, title, description, author, license, show_source} = export_params
+		{project_id, user_id, brand_variation_id, title, description, author,
+			license, show_source} = export_params
 		jobs =
 		jobs =
 			project: (cb) ->
 			project: (cb) ->
 				ProjectGetter.getProject project_id, cb
 				ProjectGetter.getProject project_id, cb
 			# TODO: when we update async, signature will change from (cb, results) to (results, cb)
 			# TODO: when we update async, signature will change from (cb, results) to (results, cb)
 			rootDoc: [ 'project', (cb, results) ->
 			rootDoc: [ 'project', (cb, results) ->
-				ProjectLocator.findRootDoc {project: results.project, project_id: project_id}, cb
+				ProjectRootDocManager.ensureRootDocumentIsSet project_id, (error) ->
+					return callback(error) if error?
+					ProjectLocator.findRootDoc {project: results.project, project_id: project_id}, cb
 			]
 			]
 			user: (cb) ->
 			user: (cb) ->
 				UserGetter.getUser user_id, {first_name: 1, last_name: 1, email: 1, overleaf: 1}, cb
 				UserGetter.getUser user_id, {first_name: 1, last_name: 1, email: 1, overleaf: 1}, cb
@@ -62,7 +66,7 @@ module.exports = ExportsHandler = self =
 						description: description
 						description: description
 						author: author
 						author: author
 						license: license
 						license: license
-						show_source: show_source
+						showSource: show_source
 				user:
 				user:
 					id: user_id
 					id: user_id
 					firstName: user.first_name
 					firstName: user.first_name

+ 15 - 3
services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee

@@ -1,5 +1,6 @@
 ProjectEntityHandler = require "./ProjectEntityHandler"
 ProjectEntityHandler = require "./ProjectEntityHandler"
 ProjectEntityUpdateHandler = require "./ProjectEntityUpdateHandler"
 ProjectEntityUpdateHandler = require "./ProjectEntityUpdateHandler"
+ProjectGetter = require "./ProjectGetter"
 Path = require "path"
 Path = require "path"
 async = require("async")
 async = require("async")
 _ = require("underscore")
 _ = require("underscore")
@@ -45,13 +46,24 @@ module.exports = ProjectRootDocManager =
 				# docpaths have a leading / so allow matching "folder/filename" and "/folder/filename"
 				# docpaths have a leading / so allow matching "folder/filename" and "/folder/filename"
 				if path == rootDocName
 				if path == rootDocName
 					root_doc_id = doc_id
 					root_doc_id = doc_id
-			# try a basename match if there was no match 
+			# try a basename match if there was no match
 			if !root_doc_id
 			if !root_doc_id
 				for doc_id, path of docPaths
 				for doc_id, path of docPaths
-					if Path.basename(path) == Path.basename(rootDocName) 
+					if Path.basename(path) == Path.basename(rootDocName)
 						root_doc_id = doc_id
 						root_doc_id = doc_id
 			# set the root doc id if we found a match
 			# set the root doc id if we found a match
 			if root_doc_id?
 			if root_doc_id?
 				ProjectEntityUpdateHandler.setRootDoc project_id, root_doc_id, callback
 				ProjectEntityUpdateHandler.setRootDoc project_id, root_doc_id, callback
 			else
 			else
-				callback()
+				callback()
+
+	ensureRootDocumentIsSet: (project_id, callback = (error) ->) ->
+		ProjectGetter.getProject project_id, rootDoc_id: 1, (error, project) ->
+			return callback(error) if error?
+			if !project?
+				return callback new Error("project not found")
+
+			if project.rootDoc_id?
+				callback()
+			else
+				ProjectRootDocManager.setRootDocAutomatically project_id, callback

+ 13 - 2
services/web/public/stylesheets/app/editor/publish-modal.less

@@ -1,12 +1,17 @@
 .modal-body-publish {
 .modal-body-publish {
+    @label-column-width: 10em;
+    @control-left-margin: 1.0em;
     .form-control-box {
     .form-control-box {
       margin-bottom: 1.5ex;
       margin-bottom: 1.5ex;
-      margin-left: 1.0em;
+      margin-left: @control-left-margin;
       label {
       label {
         display: inline-block;
         display: inline-block;
-        width: 10em;
+        width: @label-column-width;
         vertical-align: baseline;
         vertical-align: baseline;
       }
       }
+      label.checkbox-label {
+        width: auto;
+      }
       .form-control {
       .form-control {
         display: inline-block;
         display: inline-block;
         width: 60%;
         width: 60%;
@@ -26,6 +31,12 @@
       option {
       option {
         margin-left: -4px;
         margin-left: -4px;
       }
       }
+      a.help {
+        margin-left: 0.6em;
+      }
+    }
+    .no-label {
+      margin-left: @label-column-width + @control-left-margin;
     }
     }
 		#search-input-container {
 		#search-input-container {
 				overflow: hidden;
 				overflow: hidden;

+ 2 - 2
services/web/test/acceptance/coffee/ExportsTests.coffee

@@ -36,7 +36,7 @@ describe 'Exports', ->
 					description: 'description'
 					description: 'description'
 					author: 'author'
 					author: 'author'
 					license: 'other'
 					license: 'other'
-					show_source: true
+					showSource: true
 			}, (error, response, body) =>
 			}, (error, response, body) =>
 				throw error if error?
 				throw error if error?
 				expect(response.statusCode).to.equal 200
 				expect(response.statusCode).to.equal 200
@@ -53,7 +53,7 @@ describe 'Exports', ->
 			expect(project.metadata.description).to.equal 'description'
 			expect(project.metadata.description).to.equal 'description'
 			expect(project.metadata.author).to.equal 'author'
 			expect(project.metadata.author).to.equal 'author'
 			expect(project.metadata.license).to.equal 'other'
 			expect(project.metadata.license).to.equal 'other'
-			expect(project.metadata.show_source).to.equal true
+			expect(project.metadata.showSource).to.equal true
 			# version should match what was retrieved from project-history
 			# version should match what was retrieved from project-history
 			expect(project.historyVersion).to.equal @version
 			expect(project.historyVersion).to.equal @version
 			# user details should match
 			# user details should match

+ 16 - 64
services/web/test/unit/coffee/Compile/CompileManagerTests.coffee

@@ -10,7 +10,7 @@ describe "CompileManager", ->
 	beforeEach ->
 	beforeEach ->
 		@rateLimitGetStub = sinon.stub()
 		@rateLimitGetStub = sinon.stub()
 		rateLimitGetStub = @rateLimitGetStub
 		rateLimitGetStub = @rateLimitGetStub
-		@ratelimiter = 
+		@ratelimiter =
 			addCount: sinon.stub()
 			addCount: sinon.stub()
 		@CompileManager = SandboxedModule.require modulePath, requires:
 		@CompileManager = SandboxedModule.require modulePath, requires:
 			"settings-sharelatex": @settings =
 			"settings-sharelatex": @settings =
@@ -34,11 +34,11 @@ describe "CompileManager", ->
 			timeout: 42
 			timeout: 42
 		}
 		}
 
 
-	
+
 	describe "compile", ->
 	describe "compile", ->
 		beforeEach ->
 		beforeEach ->
 			@CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, false)
 			@CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, false)
-			@CompileManager._ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null)
+			@ProjectRootDocManager.ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null)
 			@CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, @limits)
 			@CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, @limits)
 			@ClsiManager.sendRequest = sinon.stub().callsArgWith(3, null, @status = "mock-status", @outputFiles = "mock output files", @output = "mock output")
 			@ClsiManager.sendRequest = sinon.stub().callsArgWith(3, null, @status = "mock-status", @outputFiles = "mock output files", @output = "mock output")
 
 
@@ -53,7 +53,7 @@ describe "CompileManager", ->
 					.should.equal true
 					.should.equal true
 
 
 			it "should ensure that the root document is set", ->
 			it "should ensure that the root document is set", ->
-				@CompileManager._ensureRootDocumentIsSet
+				@ProjectRootDocManager.ensureRootDocumentIsSet
 					.calledWith(@project_id)
 					.calledWith(@project_id)
 					.should.equal true
 					.should.equal true
 
 
@@ -81,7 +81,7 @@ describe "CompileManager", ->
 				@logger.log
 				@logger.log
 					.calledWith(project_id: @project_id, user_id: @user_id, "compiling project")
 					.calledWith(project_id: @project_id, user_id: @user_id, "compiling project")
 					.should.equal true
 					.should.equal true
-				
+
 		describe "when the project has been recently compiled", ->
 		describe "when the project has been recently compiled", ->
 			it "should return", (done)->
 			it "should return", (done)->
 				@CompileManager._checkIfAutoCompileLimitHasBeenHit = (isAutoCompile, compileGroup, cb)-> cb(null, true)
 				@CompileManager._checkIfAutoCompileLimitHasBeenHit = (isAutoCompile, compileGroup, cb)-> cb(null, true)
@@ -96,7 +96,7 @@ describe "CompileManager", ->
 				@CompileManager.compile @project_id, @user_id, {}, (err, status)->
 				@CompileManager.compile @project_id, @user_id, {}, (err, status)->
 					status.should.equal "autocompile-backoff"
 					status.should.equal "autocompile-backoff"
 					done()
 					done()
-					
+
 	describe "getProjectCompileLimits", ->
 	describe "getProjectCompileLimits", ->
 		beforeEach ->
 		beforeEach ->
 			@features = {
 			@features = {
@@ -106,17 +106,17 @@ describe "CompileManager", ->
 			@ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project = { owner_ref: @owner_id = "owner-id-123" })
 			@ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project = { owner_ref: @owner_id = "owner-id-123" })
 			@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { features: @features })
 			@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { features: @features })
 			@CompileManager.getProjectCompileLimits @project_id, @callback
 			@CompileManager.getProjectCompileLimits @project_id, @callback
-			
+
 		it "should look up the owner of the project", ->
 		it "should look up the owner of the project", ->
 			@ProjectGetter.getProject
 			@ProjectGetter.getProject
 				.calledWith(@project_id, { owner_ref: 1 })
 				.calledWith(@project_id, { owner_ref: 1 })
 				.should.equal true
 				.should.equal true
-				
+
 		it "should look up the owner's features", ->
 		it "should look up the owner's features", ->
 			@UserGetter.getUser
 			@UserGetter.getUser
 				.calledWith(@project.owner_ref, { features: 1 })
 				.calledWith(@project.owner_ref, { features: 1 })
 				.should.equal true
 				.should.equal true
-				
+
 		it "should return the limits", ->
 		it "should return the limits", ->
 			@callback
 			@callback
 				.calledWith(null, {
 				.calledWith(null, {
@@ -124,23 +124,23 @@ describe "CompileManager", ->
 					compileGroup: @group
 					compileGroup: @group
 				})
 				})
 				.should.equal true
 				.should.equal true
-				
+
 	describe "deleteAuxFiles", ->
 	describe "deleteAuxFiles", ->
 		beforeEach ->
 		beforeEach ->
 			@CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith 1, null, @limits = { compileGroup: "mock-compile-group" }
 			@CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith 1, null, @limits = { compileGroup: "mock-compile-group" }
 			@ClsiManager.deleteAuxFiles = sinon.stub().callsArg(3)
 			@ClsiManager.deleteAuxFiles = sinon.stub().callsArg(3)
 			@CompileManager.deleteAuxFiles @project_id, @user_id, @callback
 			@CompileManager.deleteAuxFiles @project_id, @user_id, @callback
-			
+
 		it "should look up the compile group to use", ->
 		it "should look up the compile group to use", ->
 			@CompileManager.getProjectCompileLimits
 			@CompileManager.getProjectCompileLimits
 				.calledWith(@project_id)
 				.calledWith(@project_id)
 				.should.equal true
 				.should.equal true
-				
+
 		it "should delete the aux files", ->
 		it "should delete the aux files", ->
 			@ClsiManager.deleteAuxFiles
 			@ClsiManager.deleteAuxFiles
 				.calledWith(@project_id, @user_id, @limits)
 				.calledWith(@project_id, @user_id, @limits)
 				.should.equal true
 				.should.equal true
-				
+
 		it "should call the callback", ->
 		it "should call the callback", ->
 			@callback.called.should.equal true
 			@callback.called.should.equal true
 
 
@@ -170,55 +170,7 @@ describe "CompileManager", ->
 
 
 			it "should call the callback with false", ->
 			it "should call the callback with false", ->
 				@callback.calledWith(null, false).should.equal true
 				@callback.calledWith(null, false).should.equal true
-				
-	describe "_ensureRootDocumentIsSet", ->
-		beforeEach ->
-			@project = {}
-			@ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project)
-			@ProjectRootDocManager.setRootDocAutomatically = sinon.stub().callsArgWith(1, null)
-			
-		describe "when the root doc is set", ->
-			beforeEach ->
-				@project.rootDoc_id = "root-doc-id"
-				@CompileManager._ensureRootDocumentIsSet(@project_id, @callback)
-
-			it "should find the project with only the rootDoc_id fiel", ->
-				@ProjectGetter.getProject
-					.calledWith(@project_id, rootDoc_id: 1)
-					.should.equal true
 
 
-			it "should not try to update the project rootDoc_id", ->
-				@ProjectRootDocManager.setRootDocAutomatically
-					.called.should.equal false
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "when the root doc is not set", ->
-			beforeEach ->
-				@CompileManager._ensureRootDocumentIsSet(@project_id, @callback)
-
-			it "should find the project with only the rootDoc_id fiel", ->
-				@ProjectGetter.getProject
-					.calledWith(@project_id, rootDoc_id: 1)
-					.should.equal true
-
-			it "should update the project rootDoc_id", ->
-				@ProjectRootDocManager.setRootDocAutomatically
-					.calledWith(@project_id)
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-		
-		describe "when the project does not exist", ->
-			beforeEach ->
-				@ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null)
-				@CompileManager._ensureRootDocumentIsSet(@project_id, @callback)
-
-			it "should call the callback with an error", ->
-				@callback.calledWith(new Error("project not found")).should.equal true
-			
 	describe "_checkIfAutoCompileLimitHasBeenHit", ->
 	describe "_checkIfAutoCompileLimitHasBeenHit", ->
 
 
 		it "should be able to compile if it is not an autocompile", (done)->
 		it "should be able to compile if it is not an autocompile", (done)->
@@ -255,16 +207,16 @@ describe "CompileManager", ->
 			@CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith 1, null, @limits = { compileGroup: "mock-compile-group" }
 			@CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith 1, null, @limits = { compileGroup: "mock-compile-group" }
 			@ClsiManager.wordCount = sinon.stub().callsArg(4)
 			@ClsiManager.wordCount = sinon.stub().callsArg(4)
 			@CompileManager.wordCount @project_id, @user_id, false, @callback
 			@CompileManager.wordCount @project_id, @user_id, false, @callback
-			
+
 		it "should look up the compile group to use", ->
 		it "should look up the compile group to use", ->
 			@CompileManager.getProjectCompileLimits
 			@CompileManager.getProjectCompileLimits
 				.calledWith(@project_id)
 				.calledWith(@project_id)
 				.should.equal true
 				.should.equal true
-				
+
 		it "should call wordCount for project", ->
 		it "should call wordCount for project", ->
 			@ClsiManager.wordCount
 			@ClsiManager.wordCount
 				.calledWith(@project_id, @user_id, false, @limits)
 				.calledWith(@project_id, @user_id, false, @limits)
 				.should.equal true
 				.should.equal true
-				
+
 		it "should call the callback", ->
 		it "should call the callback", ->
 			@callback.called.should.equal true
 			@callback.called.should.equal true

+ 1 - 1
services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee

@@ -62,7 +62,7 @@ describe 'ExportsController', ->
 			@req.body.description = description
 			@req.body.description = description
 			@req.body.author = author
 			@req.body.author = author
 			@req.body.license = license
 			@req.body.license = license
-			@req.body.show_source = true
+			@req.body.showSource = true
 
 
 		it 'should ask the handler to perform the export', (done) ->
 		it 'should ask the handler to perform the export', (done) ->
 			@handler.exportProject = sinon.stub().yields(null, {iAmAnExport: true, v1_id: 897})
 			@handler.exportProject = sinon.stub().yields(null, {iAmAnExport: true, v1_id: 897})

+ 91 - 35
services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee

@@ -8,20 +8,17 @@ SandboxedModule = require('sandboxed-module')
 describe 'ExportsHandler', ->
 describe 'ExportsHandler', ->
 
 
 	beforeEach ->
 	beforeEach ->
-		@ProjectGetter = {}
-		@ProjectLocator = {}
-		@UserGetter = {}
-		@settings = {}
 		@stubRequest = {}
 		@stubRequest = {}
 		@request = defaults: => return @stubRequest
 		@request = defaults: => return @stubRequest
 		@ExportsHandler = SandboxedModule.require modulePath, requires:
 		@ExportsHandler = SandboxedModule.require modulePath, requires:
 			'logger-sharelatex':
 			'logger-sharelatex':
 				log: ->
 				log: ->
 				err: ->
 				err: ->
-			'../Project/ProjectGetter': @ProjectGetter
-			'../Project/ProjectLocator': @ProjectLocator
-			'../User/UserGetter': @UserGetter
-			'settings-sharelatex': @settings
+			'../Project/ProjectGetter': @ProjectGetter = {}
+			'../Project/ProjectLocator': @ProjectLocator = {}
+			'../Project/ProjectRootDocManager': @ProjectRootDocManager = {}
+			'../User/UserGetter': @UserGetter = {}
+			'settings-sharelatex': @settings = {}
 			'request': @request
 			'request': @request
 		@project_id = "project-id-123"
 		@project_id = "project-id-123"
 		@project_history_id = 987
 		@project_history_id = 987
@@ -45,34 +42,49 @@ describe 'ExportsHandler', ->
 		@callback = sinon.stub()
 		@callback = sinon.stub()
 
 
 	describe 'exportProject', ->
 	describe 'exportProject', ->
-		beforeEach (done) ->
+		beforeEach ->
 			@export_data = {iAmAnExport: true}
 			@export_data = {iAmAnExport: true}
 			@response_body = {iAmAResponseBody: true}
 			@response_body = {iAmAResponseBody: true}
 			@ExportsHandler._buildExport = sinon.stub().yields(null, @export_data)
 			@ExportsHandler._buildExport = sinon.stub().yields(null, @export_data)
 			@ExportsHandler._requestExport = sinon.stub().yields(null, @response_body)
 			@ExportsHandler._requestExport = sinon.stub().yields(null, @response_body)
-			@ExportsHandler.exportProject @export_params, (error, export_data) =>
-				@callback(error, export_data)
-				done()
 
 
-		it "should build the export", ->
-			@ExportsHandler._buildExport
-			.calledWith(@export_params)
-			.should.equal true
+		describe "when all goes well", ->
+			beforeEach (done) ->
+				@ExportsHandler.exportProject @export_params, (error, export_data) =>
+					@callback(error, export_data)
+					done()
 
 
-		it "should request the export", ->
-			@ExportsHandler._requestExport
-			.calledWith(@export_data)
-			.should.equal true
+			it "should build the export", ->
+				@ExportsHandler._buildExport
+				.calledWith(@export_params)
+				.should.equal true
 
 
-		it "should return the export", ->
-			@callback
-			.calledWith(null, @export_data)
-			.should.equal true
+			it "should request the export", ->
+				@ExportsHandler._requestExport
+				.calledWith(@export_data)
+				.should.equal true
+
+			it "should return the export", ->
+				@callback
+				.calledWith(null, @export_data)
+				.should.equal true
+
+		describe "when request can't be built", ->
+			beforeEach (done) ->
+				@ExportsHandler._buildExport = sinon.stub().yields(new Error("cannot export project without root doc"))
+				@ExportsHandler.exportProject @export_params, (error, export_data) =>
+					@callback(error, export_data)
+					done()
+
+			it "should return an error", ->
+				(@callback.args[0][0] instanceof Error)
+				.should.equal true
 
 
 	describe '_buildExport', ->
 	describe '_buildExport', ->
 		beforeEach (done) ->
 		beforeEach (done) ->
 			@project =
 			@project =
 				id: @project_id
 				id: @project_id
+				rootDoc_id: 'doc1_id'
 				compiler: 'pdflatex'
 				compiler: 'pdflatex'
 				imageName: 'mock-image-name'
 				imageName: 'mock-image-name'
 				overleaf:
 				overleaf:
@@ -90,6 +102,7 @@ describe 'ExportsHandler', ->
 			@historyVersion = 777
 			@historyVersion = 777
 			@ProjectGetter.getProject = sinon.stub().yields(null, @project)
 			@ProjectGetter.getProject = sinon.stub().yields(null, @project)
 			@ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, {fileSystem: 'main.tex'}])
 			@ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, {fileSystem: 'main.tex'}])
+			@ProjectRootDocManager.ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null)
 			@UserGetter.getUser = sinon.stub().yields(null, @user)
 			@UserGetter.getUser = sinon.stub().yields(null, @user)
 			@ExportsHandler._requestVersion = sinon.stub().yields(null, @historyVersion)
 			@ExportsHandler._requestVersion = sinon.stub().yields(null, @historyVersion)
 			done()
 			done()
@@ -119,7 +132,7 @@ describe 'ExportsHandler', ->
 							description: @description
 							description: @description
 							author: @author
 							author: @author
 							license: @license
 							license: @license
-							show_source: @show_source
+							showSource: @show_source
 					user:
 					user:
 						id: @user_id
 						id: @user_id
 						firstName: @user.first_name
 						firstName: @user.first_name
@@ -159,7 +172,7 @@ describe 'ExportsHandler', ->
 							description: @description
 							description: @description
 							author: @author
 							author: @author
 							license: @license
 							license: @license
-							show_source: @show_source
+							showSource: @show_source
 					user:
 					user:
 						id: @user_id
 						id: @user_id
 						firstName: @custom_first_name
 						firstName: @custom_first_name
@@ -186,15 +199,58 @@ describe 'ExportsHandler', ->
 				.should.equal true
 				.should.equal true
 
 
 		describe "when project has no root doc", ->
 		describe "when project has no root doc", ->
-			beforeEach (done) ->
-				@ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, null])
-				@ExportsHandler._buildExport @export_params, (error, export_data) =>
-					@callback(error, export_data)
-					done()
-
-			it "should return an error", ->
-				(@callback.args[0][0] instanceof Error)
-				.should.equal true
+			describe "when a root doc can be set automatically", ->
+				beforeEach (done) ->
+					@project.rootDoc_id = null
+					@ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, {fileSystem: 'other.tex'}])
+					@ExportsHandler._buildExport @export_params, (error, export_data) =>
+						@callback(error, export_data)
+						done()
+
+				it "should set a root doc", ->
+					@ProjectRootDocManager.ensureRootDocumentIsSet.called
+					.should.equal true
+
+				it "should return export data", ->
+					expected_export_data =
+						project:
+							id: @project_id
+							rootDocPath: 'other.tex'
+							historyId: @project_history_id
+							historyVersion: @historyVersion
+							v1ProjectId: @project_history_id
+							metadata:
+								compiler: 'pdflatex'
+								imageName: 'mock-image-name'
+								title: @title
+								description: @description
+								author: @author
+								license: @license
+								showSource: @show_source
+						user:
+							id: @user_id
+							firstName: @user.first_name
+							lastName: @user.last_name
+							email: @user.email
+							orcidId: null
+							v1UserId: 876
+						destination:
+							brandVariationId: @brand_variation_id
+						options:
+							callbackUrl: null
+					@callback.calledWith(null, expected_export_data)
+					.should.equal true
+
+			describe "when no root doc can be identified", ->
+				beforeEach (done) ->
+					@ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, null])
+					@ExportsHandler._buildExport @export_params, (error, export_data) =>
+						@callback(error, export_data)
+						done()
+
+				it "should return an error", ->
+					(@callback.args[0][0] instanceof Error)
+					.should.equal true
 
 
 		describe "when user is not found", ->
 		describe "when user is not found", ->
 			beforeEach (done) ->
 			beforeEach (done) ->

+ 52 - 1
services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee

@@ -12,7 +12,8 @@ describe 'ProjectRootDocManager', ->
 		@ProjectRootDocManager = SandboxedModule.require modulePath, requires:
 		@ProjectRootDocManager = SandboxedModule.require modulePath, requires:
 			"./ProjectEntityHandler" : @ProjectEntityHandler = {}
 			"./ProjectEntityHandler" : @ProjectEntityHandler = {}
 			"./ProjectEntityUpdateHandler" : @ProjectEntityUpdateHandler = {}
 			"./ProjectEntityUpdateHandler" : @ProjectEntityUpdateHandler = {}
-	
+			"./ProjectGetter" : @ProjectGetter = {}
+
 	describe "setRootDocAutomatically", ->
 	describe "setRootDocAutomatically", ->
 		describe "when there is a suitable root doc", ->
 		describe "when there is a suitable root doc", ->
 			beforeEach (done)->
 			beforeEach (done)->
@@ -165,3 +166,53 @@ describe 'ProjectRootDocManager', ->
 
 
 			it "should not set the root doc", ->
 			it "should not set the root doc", ->
 				@ProjectEntityUpdateHandler.setRootDoc.called.should.equal false
 				@ProjectEntityUpdateHandler.setRootDoc.called.should.equal false
+
+
+	describe "ensureRootDocumentIsSet", ->
+		beforeEach ->
+			@project = {}
+			@ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project)
+			@ProjectRootDocManager.setRootDocAutomatically = sinon.stub().callsArgWith(1, null)
+
+		describe "when the root doc is set", ->
+			beforeEach ->
+				@project.rootDoc_id = "root-doc-id"
+				@ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback)
+
+			it "should find the project with only the rootDoc_id fiel", ->
+				@ProjectGetter.getProject
+					.calledWith(@project_id, rootDoc_id: 1)
+					.should.equal true
+
+			it "should not try to update the project rootDoc_id", ->
+				@ProjectRootDocManager.setRootDocAutomatically
+					.called.should.equal false
+
+			it "should call the callback", ->
+				@callback.called.should.equal true
+
+		describe "when the root doc is not set", ->
+			beforeEach ->
+				@ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback)
+
+			it "should find the project with only the rootDoc_id fiel", ->
+				@ProjectGetter.getProject
+					.calledWith(@project_id, rootDoc_id: 1)
+					.should.equal true
+
+			it "should update the project rootDoc_id", ->
+				@ProjectRootDocManager.setRootDocAutomatically
+					.calledWith(@project_id)
+					.should.equal true
+
+			it "should call the callback", ->
+				@callback.called.should.equal true
+
+		describe "when the project does not exist", ->
+			beforeEach ->
+				@ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null)
+				@ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback)
+
+			it "should call the callback with an error", ->
+				@callback.calledWith(new Error("project not found")).should.equal true
+