Browse Source

Merge branch 'master' into pdfng

Brian Gough 12 years ago
parent
commit
175de30967

+ 2 - 0
services/web/app/coffee/Features/Compile/ClsiManager.coffee

@@ -42,6 +42,8 @@ module.exports = ClsiManager =
 			return callback(error) if error?
 			if 200 <= response.statusCode < 300
 				callback null, body
+			else if response.statusCode == 413
+				callback null, compile:status:"project-too-large"
 			else
 				error = new Error("CLSI returned non-success code: #{response.statusCode}")
 				logger.error err: error, project_id: project_id, "CLSI returned failure code"

+ 36 - 29
services/web/app/coffee/Features/Compile/CompileController.coffee

@@ -13,21 +13,25 @@ module.exports = CompileController =
 		res.setTimeout(5 * 60 * 1000)
 		project_id = req.params.Project_id
 		isAutoCompile = !!req.query?.auto_compile
-		settingsOverride = req.body?.settingsOverride ? {};
-		logger.log "root doc overriden" if settingsOverride.rootDoc_id?
 		AuthenticationController.getLoggedInUserId req, (error, user_id) ->
 			return next(error) if error?
-			UserGetter.getUser user_id, {"features.compileGroup":1, "features.compileTimeout":1}, (err, user)->
-				settingsOverride.timeout = user?.features?.compileTimeout || Settings.defaultFeatures.compileTimeout
-				settingsOverride.compiler = user?.features?.compileGroup || Settings.defaultFeatures.compileGroup
-				req.session.compileGroup = settingsOverride.compiler
-				CompileManager.compile project_id, user_id, { isAutoCompile, settingsOverride }, (error, status, outputFiles) ->
-					return next(error) if error?
-					res.contentType("application/json")
-					res.send 200, JSON.stringify {
-						status: status
-						outputFiles: outputFiles
-					}
+			options = {
+				isAutoCompile: isAutoCompile
+			}
+			if req.body?.rootDoc_id?
+				options.rootDoc_id = req.body.rootDoc_id
+			else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy
+				options.rootDoc_id = req.body.settingsOverride.rootDoc_id
+			if req.body?.compiler
+				options.compiler = req.body.compiler
+			logger.log {options, project_id}, "got compile request"
+			CompileManager.compile project_id, user_id, options, (error, status, outputFiles) ->
+				return next(error) if error?
+				res.contentType("application/json")
+				res.send 200, JSON.stringify {
+					status: status
+					outputFiles: outputFiles
+				}
 
 	downloadPdf: (req, res, next = (error) ->)->
 		Metrics.inc "pdf-downloads"
@@ -40,7 +44,7 @@ module.exports = CompileController =
 			else
 				logger.log project_id: project_id, "download pdf to embed in browser"
 				res.header('Content-Disposition', "filename=#{project.getSafeProjectName()}.pdf")
-			CompileController.proxyToClsi("/project/#{project_id}/output/output.pdf", req, res, next)
+			CompileController.proxyToClsi(project_id, "/project/#{project_id}/output/output.pdf", req, res, next)
 
 	deleteAuxFiles: (req, res, next) ->
 		project_id = req.params.Project_id
@@ -55,25 +59,28 @@ module.exports = CompileController =
 				logger.err err:err, project_id:project_id, "something went wrong compile and downloading pdf"
 				res.send 500
 			url = "/project/#{project_id}/output/output.pdf"
-			CompileController.proxyToClsi url, req, res, next
+			CompileController.proxyToClsi project_id, url, req, res, next
 
 	getFileFromClsi: (req, res, next = (error) ->) ->
-		CompileController.proxyToClsi("/project/#{req.params.Project_id}/output/#{req.params.file}", req, res, next)
+		project_id = req.params.Project_id
+		CompileController.proxyToClsi(project_id, "/project/#{project_id}/output/#{req.params.file}", req, res, next)
 
 	proxySync: (req, res, next = (error) ->) ->
-		CompileController.proxyToClsi(req.url, req, res, next)
+		CompileController.proxyToClsi(req.params.Project_id, req.url, req, res, next)
 
-	proxyToClsi: (url, req, res, next = (error) ->) ->
-		if req.session.compileGroup == "priority"
-			compilerUrl = Settings.apis.clsi_priority.url
-		else
-			compilerUrl = Settings.apis.clsi.url
-		url = "#{compilerUrl}#{url}"
-		logger.log url: url, "proxying to CLSI"
-		oneMinute = 60 * 1000
-		proxy = request(url: url, method: req.method, timeout: oneMinute)
-		proxy.pipe(res)
-		proxy.on "error", (error) ->
-			logger.warn err: error, url: url, "CLSI proxy error"
+	proxyToClsi: (project_id, url, req, res, next = (error) ->) ->
+		CompileManager.getProjectCompileLimits project_id, (error, limits) ->
+			return next(error) if error?
+			if limits.compileGroup == "priority"
+				compilerUrl = Settings.apis.clsi_priority.url
+			else
+				compilerUrl = Settings.apis.clsi.url
+			url = "#{compilerUrl}#{url}"
+			logger.log url: url, "proxying to CLSI"
+			oneMinute = 60 * 1000
+			proxy = request(url: url, method: req.method, timeout: oneMinute)
+			proxy.pipe(res)
+			proxy.on "error", (error) ->
+				logger.warn err: error, url: url, "CLSI proxy error"
 
 	

+ 19 - 5
services/web/app/coffee/Features/Compile/CompileManager.coffee

@@ -6,19 +6,20 @@ rclient = redis.createClient(Settings.redis.web)
 DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
 Project = require("../../models/Project").Project
 ProjectRootDocManager = require "../Project/ProjectRootDocManager"
+UserGetter = require "../User/UserGetter"
 ClsiManager = require "./ClsiManager"
 Metrics = require('../../infrastructure/Metrics')
 logger = require("logger-sharelatex")
 rateLimiter = require("../../infrastructure/RateLimiter")
 
 module.exports = CompileManager =
-	compile: (project_id, user_id, opt = {}, _callback = (error) ->) ->
+	compile: (project_id, user_id, options = {}, _callback = (error) ->) ->
 		timer = new Metrics.Timer("editor.compile")
 		callback = (args...) ->
 			timer.done()
 			_callback(args...)
 
-		@_checkIfAutoCompileLimitHasBeenHit opt.isAutoCompile, (err, canCompile)->
+		@_checkIfAutoCompileLimitHasBeenHit options.isAutoCompile, (err, canCompile)->
 			if !canCompile
 				return callback null, "autocompile-backoff", []
 			logger.log project_id: project_id, user_id: user_id, "compiling project"
@@ -31,11 +32,24 @@ module.exports = CompileManager =
 					return callback(error) if error?
 					DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
 						return callback(error) if error?
-						ClsiManager.sendRequest project_id, opt.settingsOverride, (error, status, outputFiles) ->
+						CompileManager.getProjectCompileLimits project_id, (error, limits) ->
 							return callback(error) if error?
-							logger.log files: outputFiles, "output files"
-							callback(null, status, outputFiles)
+							for key, value of limits
+								options[key] = value
+							ClsiManager.sendRequest project_id, options, (error, status, outputFiles, output) ->
+								return callback(error) if error?
+								logger.log files: outputFiles, "output files"
+								callback(null, status, outputFiles, output)
 
+	getProjectCompileLimits: (project_id, callback = (error, limits) ->) ->
+		Project.findById project_id, {owner_ref: 1}, (error, project) ->
+			return callback(error) if error?
+			UserGetter.getUser project.owner_ref, {"features":1}, (err, owner)->
+				return callback(error) if error?
+				callback null, {
+					timeout: owner.features?.compileTimeout || Settings.defaultFeatures.compileTimeout
+					compileGroup: owner.features?.compileGroup || Settings.defaultFeatures.compileGroup
+				}
 
 	getLogLines: (project_id, callback)->
 		Metrics.inc "editor.raw-logs"

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

@@ -195,13 +195,13 @@ module.exports = ProjectEntityHandler =
 						tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result.path.fileSystem, rev:fileRef.rev, project_name:project.name}, (error) ->
 							callback(error, fileRef, folder_id)
 
-	mkdirp: (project_or_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
+	mkdirp: (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
 		self = @
 		folders = path.split('/')
 		folders = _.select folders, (folder)->
 			return folder.length != 0
 
-		Project.getProject project_or_id, "", (err, project)=>
+		ProjectGetter.getProjectWithoutDocLines project_id, (err, project)=>
 			if path == '/'
 				logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder"
 				return callback(null, [], project.rootFolder[0])
@@ -214,10 +214,10 @@ module.exports = ProjectEntityHandler =
 				if parentFolder?  
 					parentFolder_id = parentFolder._id
 				builtUpPath = "#{builtUpPath}/#{folderName}"
-				projectLocator.findElementByPath project_or_id, builtUpPath, (err, foundFolder)=>
+				projectLocator.findElementByPath project_id, builtUpPath, (err, foundFolder)=>
 					if !foundFolder?
 						logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp"
-						@addFolder project_or_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)->
+						@addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)->
 							newFolder.parentFolder_id = parentFolder_id
 							previousFolders.push newFolder
 							callback null, previousFolders

+ 20 - 9
services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee

@@ -1,18 +1,29 @@
 ProjectEntityHandler = require "./ProjectEntityHandler"
 Path = require "path"
+async = require("async")
+_ = require("underscore")
 
 module.exports = ProjectRootDocManager =
 	setRootDocAutomatically: (project_id, callback = (error) ->) ->
+
 		ProjectEntityHandler.getAllDocs project_id, (error, docs) ->
 			return callback(error) if error?
+
+
 			root_doc_id = null
-			for path, doc of docs
-				for line in doc.lines || []
-					match = line.match /(.*)\\documentclass/ # no lookbehind in js regexp :(
-					if Path.extname(path).match(/\.R?tex$/) and match and !match[1].match /%/
-						root_doc_id = doc._id
-			if root_doc_id?
-				ProjectEntityHandler.setRootDoc project_id, root_doc_id, callback
-			else
-				callback()
+			jobs = _.map docs, (doc, path)->
+				return (cb)->
+					rootDocId = null
+					for line in doc.lines || []
+						match = line.match /(.*)\\documentclass/ # no lookbehind in js regexp :(
+						isRootDoc = Path.extname(path).match(/\.R?tex$/) and match and !match[1].match /%/
+						if isRootDoc
+							rootDocId = doc?._id
+					cb(rootDocId)
+
+			async.series jobs, (root_doc_id)->
+				if root_doc_id?
+					ProjectEntityHandler.setRootDoc project_id, root_doc_id, callback
+				else
+					callback()
 

+ 8 - 0
services/web/app/views/project/editor/pdf.jade

@@ -102,6 +102,14 @@ div.full-size.pdf(ng-controller="PdfController")
 							ng-click="hello('compile-timeout')"
 						) #{translate("start_free_trial")}
 
+	.pdf-errors(ng-show="pdf.projectTooLarge")
+		.alert.alert-danger
+			strong #{translate("project_too_large")} 
+			span #{translate("project_too_large_please_reduce")}
+		
+
+		
+
 
 
 	.pdf-logs(ng-show="(pdf.view == 'logs' || pdf.failure) && !pdf.error && !pdf.timeout && !pdf.uncompiled")

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

@@ -8,7 +8,7 @@ block content
 					.card
 						.page-header
 							h1 #{translate("log_in")}
-						form(async-form="login", name="loginForm", action='/login', ng-cloak)
+						form(async-form="login", name="loginForm", action='/login', method="POST", ng-cloak)
 							input(name='_csrf', type='hidden', value=csrfToken)
 							input(name='redir', type='hidden', value=redir)
 							form-messages(for="loginForm")

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

@@ -13,6 +13,7 @@ block content
 							async-form="password-reset-request",
 							name="passwordResetForm"
 							action="/user/password/reset",
+							method="POST",
 							ng-cloak
 						)
 							input(type="hidden", name="_csrf", value=csrfToken)

+ 1 - 1
services/web/app/views/user/register.jade

@@ -20,7 +20,7 @@ block content
 					.card
 						.page-header
 							h1 #{translate("register")}
-						form(async-form="register", name="registerForm", action="/register", ng-cloak)
+						form(async-form="register", name="registerForm", action="/register", method="POST", ng-cloak)
 							input(name='_csrf', type='hidden', value=csrfToken)
 							input(name='redir', type='hidden', value=redir)
 							form-messages(for="registerForm")

+ 2 - 1
services/web/app/views/user/setPassword.jade

@@ -12,6 +12,7 @@ block content
 							async-form="password-reset",
 							name="passwordResetForm",
 							action="/user/password/set",
+							method="POST",
 							ng-cloak
 						)
 							input(type="hidden", name="_csrf", value=csrfToken)
@@ -41,4 +42,4 @@ block content
 								button.btn.btn-primary(
 									type='submit',
 									ng-disabled="passwordResetForm.$invalid"
-								) #{translate("set_new_password")}
+								) #{translate("set_new_password")}

+ 2 - 2
services/web/app/views/user/settings.jade

@@ -17,7 +17,7 @@ block content
 								.row
 									.col-md-5
 										h3  #{translate("update_account_info")}
-										form(async-form="settings", name="settingsForm", action="/user/settings", novalidate)
+										form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
 											input(type="hidden", name="_csrf", value=csrfToken)
 											.form-group
 												label(for='email')  #{translate("email")}
@@ -54,7 +54,7 @@ block content
 
 									.col-md-5.col-md-offset-1
 										h3  #{translate("change_password")}
-										form(async-form="changepassword", name="changePasswordForm", action="/user/password/update", novalidate)
+										form(async-form="changepassword", name="changePasswordForm", action="/user/password/update", method="POST", novalidate)
 											input(type="hidden", name="_csrf", value=csrfToken)
 											.form-group
 												label(for='currentPassword') #{translate("current_password")}

+ 6 - 0
services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee

@@ -17,12 +17,18 @@ define [
 
 			onChange = (e) =>
 				@runCheckOnChange(e)
+				
+			onScroll = () =>
+				@closeContextMenu()
 
 			@editor.on "changeSession", (e) =>
 				@runSpellCheckSoon(200)
 
 				e.oldSession?.getDocument().off "change", onChange
 				e.session.getDocument().on "change", onChange
+				
+				e.oldSession?.off "changeScrollTop", onScroll
+				e.session.on "changeScrollTop", onScroll
 
 			@$scope.spellingMenu = {left: '0px', top: '0px'}
 

+ 0 - 82
services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellingMenuView.coffee

@@ -1,82 +0,0 @@
-define [
-	"libs/backbone"
-	"libs/mustache"
-], () ->
-	SUGGESTIONS_TO_SHOW = 5
-
-	SpellingMenuView = Backbone.View.extend
-		templates:
-			menu: $("#spellingMenuTemplate").html()
-			entry: $("#spellingMenuEntryTemplate").html()
-
-		events:
-			"click a#learnWord": ->
-				@trigger "click:learn", @_currentHighlight
-				@hide()
-
-		initialize: (options) ->
-			@ide = options.ide
-			@ide.editor.getContainerElement().append @render().el
-			@ide.editor.on "click", () => @hide()
-			@ide.editor.on "scroll", () => @hide()
-			@ide.editor.on "update:doc", () => @hide()
-			@ide.editor.on "change:doc", () => @hide()
-
-		render: () ->
-			@setElement($(@templates.menu))
-			@$el.css "z-index" : 10000
-			@$(".dropdown-toggle").dropdown()
-			@hide()
-			return @
-
-		showForHighlight: (highlight) ->
-			if @_currentHighlight? and highlight != @_currentHighlight
-				@_close()
-			
-			if !@_currentHighlight?
-				@_currentHighlight = highlight
-				@_setSuggestions(highlight)
-				position = @ide.editor.textToEditorCoordinates(
-					highlight.row
-					highlight.column + highlight.word.length
-				)
-				@_position(position.x, position.y)
-				@_show()
-
-		hideIfAppropriate: (cursorPosition) ->
-			if @_currentHighlight?
-				if !@_cursorCloseToHighlight(cursorPosition, @_currentHighlight) and !@_isOpen()
-					@hide()
-
-		hide: () ->
-			delete @_currentHighlight
-			@_close()
-			@$el.hide()
-
-		_setSuggestions: (highlight) ->
-			@$(".spelling-suggestion").remove()
-			divider = @$(".divider")
-			for suggestion in highlight.suggestions.slice(0, SUGGESTIONS_TO_SHOW)
-				do (suggestion) =>
-					entry = $(Mustache.to_html(@templates.entry, word: suggestion))
-					divider.before(entry)
-					entry.on "click", () =>
-						@trigger "click:suggestion", suggestion, highlight
-
-		_show: () -> @$el.show()
-
-		_isOpen: () ->
-			@$(".dropdown-menu").is(":visible")
-
-		_close: () ->
-			if @_isOpen()
-				@$el.dropdown("toggle")
-
-		_cursorCloseToHighlight: (position, highlight) ->
-			position.row == highlight.row and
-			position.column >= highlight.column and
-			position.column <= highlight.column + highlight.word.length + 1
-
-		_position: (x,y) -> @$el.css left: x, top: y
-
-

+ 3 - 2
services/web/public/coffee/ide/pdf/controllers/PdfController.coffee

@@ -15,8 +15,7 @@ define [
 			if options.isAutoCompile
 				url += "?auto_compile=true"
 			return $http.post url, {
-				settingsOverride:
-					rootDoc_id: options.rootDocOverride_id or null
+				rootDoc_id: options.rootDocOverride_id or null
 				_csrf: window.csrfToken
 			}
 
@@ -32,6 +31,8 @@ define [
 				$scope.pdf.timedout = true
 			else if response.status == "autocompile-backoff"
 				$scope.pdf.uncompiled = true
+			else if response.status == "project-too-large"
+				$scope.pdf.projectTooLarge = true
 			else if response.status == "failure"
 				$scope.pdf.failure = true
 				fetchLogs()

+ 1 - 1
services/web/public/js/ace/mode-latex.js

@@ -15,7 +15,7 @@ var LatexHighlightRules = function() {
             regex : "(\\\\(?:documentclass|usepackage))(?:(\\[)([^\\]]*)(\\]))?({)([^}]*)(})"
         }, {
             token : ["keyword","lparen", "variable.parameter", "rparen"],
-            regex : "(\\\\(?:label|ref|cite(?:[^{]*)))(?:({)([^}]*)(}))?"
+            regex : "(\\\\(?:label|v?ref|cite(?:[^{]*)))(?:({)([^}]*)(}))?"
         }, {
             token : ["storage.type", "lparen", "variable.parameter", "rparen"],
             regex : "(\\\\(?:begin|end))({)(\\w*)(})"

+ 12 - 23
services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee

@@ -21,7 +21,7 @@ describe "CompileController", ->
 					clsi:
 						url: "clsi.example.com"
 					clsi_priority:
-						url: "clsi.example.com"
+						url: "clsi-priority.example.com"
 			"request": @request = sinon.stub()
 			"../../models/Project": Project: @Project = {}
 			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
@@ -47,8 +47,7 @@ describe "CompileController", ->
 					Project_id: @project_id
 				@req.session = {}
 				@AuthenticationController.getLoggedInUserId = sinon.stub().callsArgWith(1, null, @user_id = "mock-user-id")
-				@CompileManager.compile = sinon.stub().callsArgWith(3, null, @status = "success", @outputFiles = ["mock-output-files"])
-				@UserGetter.getUser.callsArgWith(2, null, @user)
+				@CompileManager.compile = sinon.stub().callsArgWith(3, null, @status = "success", @outputFiles = ["mock-output-files"], @output = "mock-output")
 				@CompileController.compile @req, @res, @next
 
 			it "should look up the user id", ->
@@ -58,7 +57,7 @@ describe "CompileController", ->
 
 			it "should do the compile without the auto compile flag", ->
 				@CompileManager.compile
-					.calledWith(@project_id, @user_id, { isAutoCompile: false, settingsOverride:{timeout:@user.features.compileTimeout, compiler:@user.features.compileGroup} })
+					.calledWith(@project_id, @user_id, { isAutoCompile: false })
 					.should.equal true
 
 			it "should set the content-type of the response to application/json", ->
@@ -73,16 +72,6 @@ describe "CompileController", ->
 					outputFiles: @outputFiles
 				})
 
-			it "should get the compile timeout from the users features",->
-				@UserGetter.getUser.args[0][0].should.equal @user_id
-				assert.deepEqual @UserGetter.getUser.args[0][1], {"features.compileGroup":1, "features.compileTimeout":1}
-
-			it "should put the compile group on the req", ->
-				@req.session.compileGroup.should.equal @user.features.compileGroup
-
-			it "should set the timeout", ->
-				assert @res.timout > 1000 * 60 * 3
-
 		describe "when an auto compile", ->
 			beforeEach ->
 				@req.params =
@@ -91,11 +80,12 @@ describe "CompileController", ->
 					auto_compile: "true"
 				@AuthenticationController.getLoggedInUserId = sinon.stub().callsArgWith(1, null, @user_id = "mock-user-id")
 				@CompileManager.compile = sinon.stub().callsArgWith(3, null, @status = "success", @outputFiles = ["mock-output-files"])
-				@UserGetter.getUser.callsArgWith(2, null, @user)
 				@CompileController.compile @req, @res, @next
 
 			it "should do the compile with the auto compile flag", ->
-				@CompileManager.compile.calledWith(@project_id, @user_id, { isAutoCompile: true, settingsOverride:{timeout:@user.features.compileTimeout, compiler:@user.features.compileGroup} }).should.equal true
+				@CompileManager.compile
+					.calledWith(@project_id, @user_id, { isAutoCompile: true })
+					.should.equal true
 
 	describe "downloadPdf", ->
 		beforeEach ->
@@ -134,7 +124,7 @@ describe "CompileController", ->
 
 			it "should proxy the PDF from the CLSI", ->
 				@CompileController.proxyToClsi
-					.calledWith("/project/#{@project_id}/output/output.pdf", @req, @res, @next)
+					.calledWith(@project_id, "/project/#{@project_id}/output/output.pdf", @req, @res, @next)
 					.should.equal true
 
 	describe "proxyToClsi", ->
@@ -152,8 +142,8 @@ describe "CompileController", ->
 		describe "user with standard priority", ->
 
 			beforeEach ->
-				@UserGetter.getUser.callsArgWith(2, null, @user)
-				@CompileController.proxyToClsi(@url = "/test", @req, @res, @next)	
+				@CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, {compileGroup: "standard"})
+				@CompileController.proxyToClsi(@project_id, @url = "/test", @req, @res, @next)	
 
 
 			it "should open a request to the CLSI", ->
@@ -176,9 +166,8 @@ describe "CompileController", ->
 		describe "user with priority compile", ->
 
 			beforeEach ->
-				@req.session.compileGroup = "priority"
-				@UserGetter.getUser.callsArgWith(2, null, @user)
-				@CompileController.proxyToClsi(@url = "/test", @req, @res, @next)	
+				@CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, {compileGroup: "priority"})
+				@CompileController.proxyToClsi(@project_id, @url = "/test", @req, @res, @next)	
 
 			it "should proxy to the priorty url if the user has the feature", ()->
 				@request
@@ -225,5 +214,5 @@ describe "CompileController", ->
 
 		it "should proxy the res to the clsi with correct url", (done)->
 			@CompileController.compileAndDownloadPdf @req, @res
-			@CompileController.proxyToClsi.calledWith("/project/#{@project_id}/output/output.pdf", @req, @res).should.equal true
+			@CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/output/output.pdf", @req, @res).should.equal true
 			done()

+ 46 - 5
services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee

@@ -20,6 +20,7 @@ describe "CompileManager", ->
 			"../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler = {}
 			"../Project/ProjectRootDocManager": @ProjectRootDocManager = {}
 			"../../models/Project": Project: @Project = {}
+			"../User/UserGetter": @UserGetter = {}
 			"./ClsiManager": @ClsiManager = {}
 			"../../infrastructure/RateLimiter": @ratelimiter
 			"../../infrastructure/Metrics": @Metrics =
@@ -30,13 +31,18 @@ describe "CompileManager", ->
 		@project_id = "mock-project-id-123"
 		@user_id = "mock-user-id-123"
 		@callback = sinon.stub()
+		@limits = {
+			timeout: 42
+		}
 
+	
 	describe "compile", ->
 		beforeEach ->
 			@CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, false)
 			@CompileManager._ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null)
 			@DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null)
-			@ClsiManager.sendRequest = sinon.stub().callsArgWith(2, null, @status = "mock-status")
+			@CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, @limits)
+			@ClsiManager.sendRequest = sinon.stub().callsArgWith(2, null, @status = "mock-status", @outputFiles = "mock output files", @output = "mock output")
 
 		describe "succesfully", ->
 			beforeEach ->
@@ -58,14 +64,21 @@ describe "CompileManager", ->
 					.calledWith(@project_id)
 					.should.equal true
 
-			it "should run the compile with the new compiler API", ->
-				@ClsiManager.sendRequest
+			it "should get the project compile limits", ->
+				@CompileManager.getProjectCompileLimits
 					.calledWith(@project_id)
 					.should.equal true
 
-			it "should call the callback", ->
+			it "should run the compile with the compile limits", ->
+				@ClsiManager.sendRequest
+					.calledWith(@project_id, {
+						timeout: @limits.timeout
+					})
+					.should.equal true
+
+			it "should call the callback with the output", ->
 				@callback
-					.calledWith(null, @status)
+					.calledWith(null, @status, @outputFiles, @output)
 					.should.equal true
 
 			it "should time the compile", ->
@@ -93,6 +106,34 @@ describe "CompileManager", ->
 				@CompileManager.compile @project_id, @user_id, {}, (err, status)->
 					status.should.equal "autocompile-backoff"
 					done()
+					
+	describe "getProjectCompileLimits", ->
+		beforeEach ->
+			@features = {
+				compileTimeout:   @timeout = 42
+				compileGroup:     @group = "priority"
+			}
+			@Project.findById = sinon.stub().callsArgWith(2, null, @project = { owner_ref: @owner_id = "owner-id-123" })
+			@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { features: @features })
+			@CompileManager.getProjectCompileLimits @project_id, @callback
+			
+		it "should look up the owner of the project", ->
+			@Project.findById
+				.calledWith(@project_id, { owner_ref: 1 })
+				.should.equal true
+				
+		it "should look up the owner's features", ->
+			@UserGetter.getUser
+				.calledWith(@project.owner_ref, { features: 1 })
+				.should.equal true
+				
+		it "should return the limits", ->
+			@callback
+				.calledWith(null, {
+					timeout:      @timeout
+					compileGroup: @group
+				})
+				.should.equal true
 
 	describe "getLogLines", ->
 		beforeEach ->

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

@@ -77,6 +77,7 @@ describe 'ProjectEntityHandler', ->
 			@parentFolder_id = "1jnjknjk"
 			@newFolder = {_id:"newFolder_id_here"}
 			@lastFolder = {_id:"123das", folders:[]}
+			@ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project)
 			@projectLocator.findElementByPath = (project_id, path, cb)=>
 				@parentFolder = {_id:"parentFolder_id_here"}
 				lastFolder = path.substring(path.lastIndexOf("/"))

+ 13 - 12
services/web/test/UnitTests/coffee/Project/ProjectRootDocManagerTests.coffee

@@ -14,17 +14,24 @@ describe 'ProjectRootDocManager', ->
 	
 	describe "setRootDocAutomatically", ->
 		describe "when there is a suitable root doc", ->
-			beforeEach ->
+			beforeEach (done)->
 				@docs =
 					"/chapter1.tex":
 						_id: "doc-id-1"
-						lines: ["\\begin{document}", "Hello world", "\\end{document}"]
+						lines: ["something else","\\begin{document}", "Hello world", "\\end{document}"]
 					"/main.tex":
 						_id: "doc-id-2"
-						lines: ["\\documentclass{article}", "\\input{chapter1}"]
+						lines: ["different line","\\documentclass{article}", "\\input{chapter1}"]
+					"/nested/chapter1a.tex":
+						_id: "doc-id-3"
+						lines: ["Hello world"]
+					"/nested/chapter1b.tex":
+						_id: "doc-id-4"
+						lines: ["Hello world"]
+
 				@ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs)
 				@ProjectEntityHandler.setRootDoc = sinon.stub().callsArgWith(2)
-				@ProjectRootDocManager.setRootDocAutomatically @project_id, @callback
+				@ProjectRootDocManager.setRootDocAutomatically @project_id, done
 
 			it "should check the docs of the project", ->
 				@ProjectEntityHandler.getAllDocs.calledWith(@project_id)
@@ -34,9 +41,6 @@ describe 'ProjectRootDocManager', ->
 				@ProjectEntityHandler.setRootDoc.calledWith(@project_id, "doc-id-2")
 					.should.equal true
 
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
 		describe "when the root doc is an Rtex file", ->
 			beforeEach ->
 				@docs =
@@ -55,7 +59,7 @@ describe 'ProjectRootDocManager', ->
 					.should.equal true
 
 		describe "when there is no suitable root doc", ->
-			beforeEach ->
+			beforeEach (done)->
 				@docs =
 					"/chapter1.tex":
 						_id: "doc-id-1"
@@ -65,11 +69,8 @@ describe 'ProjectRootDocManager', ->
 						lines: ["%Example: \\documentclass{article}"]
 				@ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs)
 				@ProjectEntityHandler.setRootDoc = sinon.stub().callsArgWith(2)
-				@ProjectRootDocManager.setRootDocAutomatically @project_id, @callback
+				@ProjectRootDocManager.setRootDocAutomatically @project_id, done
 
 			it "should not set the root doc to the doc containing a documentclass", ->
 				@ProjectEntityHandler.setRootDoc.called.should.equal false
 
-			it "should call the callback", ->
-				@callback.called.should.equal true
-