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

Merge branch 'master' into master-redesign

Conflicts:
	public/stylesheets/less/trackchanges.less
James Allen 12 лет назад
Родитель
Сommit
b6217c401e
30 измененных файлов с 356 добавлено и 56 удалено
  1. 8 3
      services/web/app/coffee/Features/Compile/ClsiManager.coffee
  2. 3 1
      services/web/app/coffee/Features/Compile/CompileController.coffee
  3. 2 2
      services/web/app/coffee/Features/Compile/CompileManager.coffee
  4. 7 2
      services/web/app/coffee/Features/Docstore/DocstoreManager.coffee
  5. 21 0
      services/web/app/coffee/Features/Editor/EditorHttpController.coffee
  6. 1 0
      services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
  7. 12 2
      services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
  8. 2 1
      services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee
  9. 3 0
      services/web/app/coffee/router.coffee
  10. 14 2
      services/web/app/views/templates.jade
  11. 29 0
      services/web/public/coffee/file-tree/DeletedDocsFolderView.coffee
  12. 1 0
      services/web/public/coffee/file-tree/EntityView.coffee
  13. 29 3
      services/web/public/coffee/file-tree/FileTreeManager.coffee
  14. 1 0
      services/web/public/coffee/models/Doc.coffee
  15. 3 3
      services/web/public/coffee/models/Folder.coffee
  16. 11 1
      services/web/public/coffee/models/Project.coffee
  17. 6 1
      services/web/public/coffee/pdf/CompiledView.coffee
  18. 6 3
      services/web/public/coffee/pdf/PdfManager.coffee
  19. 32 18
      services/web/public/coffee/track-changes/DiffView.coffee
  20. 28 2
      services/web/public/coffee/track-changes/TrackChangesManager.coffee
  21. 1 1
      services/web/public/coffee/track-changes/models/Change.coffee
  22. 1 1
      services/web/public/coffee/track-changes/models/Diff.coffee
  23. 8 0
      services/web/public/stylesheets/app/editor.less
  24. 28 6
      services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee
  25. 2 2
      services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
  26. 1 1
      services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee
  27. 16 0
      services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee
  28. 46 0
      services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee
  29. 8 0
      services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee
  30. 26 1
      services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee

+ 8 - 3
services/web/app/coffee/Features/Compile/ClsiManager.coffee

@@ -8,8 +8,8 @@ logger = require "logger-sharelatex"
 url = require("url")
 url = require("url")
 
 
 module.exports = ClsiManager =
 module.exports = ClsiManager =
-	sendRequest: (project_id, callback = (error, success) ->) ->
-		ClsiManager._buildRequest project_id, (error, req) ->
+	sendRequest: (project_id, settingsOverride = {}, callback = (error, success) ->) ->
+		ClsiManager._buildRequest project_id, settingsOverride, (error, req) ->
 			return callback(error) if error?
 			return callback(error) if error?
 			logger.log project_id: project_id, "sending compile to CLSI"
 			logger.log project_id: project_id, "sending compile to CLSI"
 			ClsiManager._postToClsi project_id, req, (error, response) ->
 			ClsiManager._postToClsi project_id, req, (error, response) ->
@@ -52,7 +52,7 @@ module.exports = ClsiManager =
 		return outputFiles
 		return outputFiles
 
 
 	VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
 	VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
-	_buildRequest: (project_id, callback = (error, request) ->) ->
+	_buildRequest: (project_id, settingsOverride={}, callback = (error, request) ->) ->
 		Project.findById project_id, {compiler: 1, rootDoc_id: 1}, (error, project) ->
 		Project.findById project_id, {compiler: 1, rootDoc_id: 1}, (error, project) ->
 			return callback(error) if error?
 			return callback(error) if error?
 			return callback(new Errors.NotFoundError("project does not exist: #{project_id}")) if !project?
 			return callback(new Errors.NotFoundError("project does not exist: #{project_id}")) if !project?
@@ -67,6 +67,7 @@ module.exports = ClsiManager =
 
 
 					resources = []
 					resources = []
 					rootResourcePath = null
 					rootResourcePath = null
+					rootResourcePathOverride = null
 
 
 					for path, doc of docs
 					for path, doc of docs
 						path = path.replace(/^\//, "") # Remove leading /
 						path = path.replace(/^\//, "") # Remove leading /
@@ -75,6 +76,10 @@ module.exports = ClsiManager =
 							content: doc.lines.join("\n")
 							content: doc.lines.join("\n")
 						if project.rootDoc_id? and doc._id.toString() == project.rootDoc_id.toString()
 						if project.rootDoc_id? and doc._id.toString() == project.rootDoc_id.toString()
 							rootResourcePath = path
 							rootResourcePath = path
+						if settingsOverride.rootDoc_id? and doc._id.toString() == settingsOverride.rootDoc_id.toString()
+							rootResourcePathOverride = path
+
+					rootResourcePath = rootResourcePathOverride if rootResourcePathOverride?
 
 
 					for path, file of files
 					for path, file of files
 						path = path.replace(/^\//, "") # Remove leading /
 						path = path.replace(/^\//, "") # Remove leading /

+ 3 - 1
services/web/app/coffee/Features/Compile/CompileController.coffee

@@ -11,9 +11,11 @@ module.exports = CompileController =
 	compile: (req, res, next = (error) ->) ->
 	compile: (req, res, next = (error) ->) ->
 		project_id = req.params.Project_id
 		project_id = req.params.Project_id
 		isAutoCompile = !!req.query?.auto_compile
 		isAutoCompile = !!req.query?.auto_compile
+		settingsOverride = req.body?.settingsOverride ? {};
+		logger.log "root doc overriden" if settingsOverride.rootDoc_id?
 		AuthenticationController.getLoggedInUserId req, (error, user_id) ->
 		AuthenticationController.getLoggedInUserId req, (error, user_id) ->
 			return next(error) if error?
 			return next(error) if error?
-			CompileManager.compile project_id, user_id, { isAutoCompile }, (error, status, outputFiles) ->
+			CompileManager.compile project_id, user_id, { isAutoCompile, settingsOverride }, (error, status, outputFiles) ->
 				return next(error) if error?
 				return next(error) if error?
 				res.contentType("application/json")
 				res.contentType("application/json")
 				res.send 200, JSON.stringify {
 				res.send 200, JSON.stringify {

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

@@ -25,12 +25,12 @@ module.exports = CompileManager =
 				return callback(error) if error?
 				return callback(error) if error?
 				if recentlyCompiled
 				if recentlyCompiled
 					return callback new Error("project was recently compiled so not continuing")
 					return callback new Error("project was recently compiled so not continuing")
-
+				
 				CompileManager._ensureRootDocumentIsSet project_id, (error) ->
 				CompileManager._ensureRootDocumentIsSet project_id, (error) ->
 					return callback(error) if error?
 					return callback(error) if error?
 					DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
 					DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
 						return callback(error) if error?
 						return callback(error) if error?
-						ClsiManager.sendRequest project_id, (error, status, outputFiles) ->
+						ClsiManager.sendRequest project_id, opt.settingsOverride, (error, status, outputFiles) ->
 							return callback(error) if error?
 							return callback(error) if error?
 							logger.log files: outputFiles, "output files"
 							logger.log files: outputFiles, "output files"
 							callback(null, status, outputFiles)
 							callback(null, status, outputFiles)

+ 7 - 2
services/web/app/coffee/Features/Docstore/DocstoreManager.coffee

@@ -30,9 +30,14 @@ module.exports = DocstoreManager =
 				logger.error err: error, project_id: project_id, "error getting all docs from docstore"
 				logger.error err: error, project_id: project_id, "error getting all docs from docstore"
 				callback(error)
 				callback(error)
 
 
-	getDoc: (project_id, doc_id, callback = (error, lines, rev) ->) ->
-		logger.log project_id: project_id, doc_id: doc_id, "getting doc in docstore api"
+	getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev) ->) ->
+		if typeof(options) == "function"
+			callback = options
+			options = {}
+		logger.log project_id: project_id, doc_id: doc_id, options: options, "getting doc in docstore api"
 		url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
 		url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
+		if options.include_deleted
+			url += "?include_deleted=true"
 		request.get {
 		request.get {
 			url: url
 			url: url
 			json: true
 			json: true

+ 21 - 0
services/web/app/coffee/Features/Editor/EditorHttpController.coffee

@@ -0,0 +1,21 @@
+ProjectEntityHandler = require "../Project/ProjectEntityHandler"
+logger = require "logger-sharelatex"
+EditorRealTimeController = require "./EditorRealTimeController"
+
+module.exports = EditorHttpController =
+	restoreDoc: (req, res, next) ->
+		project_id = req.params.Project_id
+		doc_id = req.params.doc_id
+		name = req.body.name
+
+		if !name?
+			return res.send 400 # Malformed request
+
+		logger.log project_id: project_id, doc_id: doc_id, "restoring doc"
+		ProjectEntityHandler.restoreDoc project_id, doc_id, name, (err, doc, folder_id) =>
+			return next(error) if error?
+			EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc)
+			res.json {
+				doc_id: doc._id
+			}
+

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

@@ -15,6 +15,7 @@ module.exports = ProjectEditorHandler =
 			description: project.description
 			description: project.description
 			spellCheckLanguage: project.spellCheckLanguage
 			spellCheckLanguage: project.spellCheckLanguage
 			deletedByExternalDataSource : project.deletedByExternalDataSource || false
 			deletedByExternalDataSource : project.deletedByExternalDataSource || false
+			deletedDocs: project.deletedDocs
 
 
 		if options.includeUsers
 		if options.includeUsers
 			result.features =
 			result.features =

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

@@ -111,8 +111,11 @@ module.exports = ProjectEntityHandler =
 		logger.log sl_req_id: sl_req_id, project_id: project_id, "removing root doc"
 		logger.log sl_req_id: sl_req_id, project_id: project_id, "removing root doc"
 		Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback
 		Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback
 
 
-	getDoc: (project_id, doc_id, callback = (error, lines, rev) ->) ->
-		DocstoreManager.getDoc project_id, doc_id, callback
+	getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev) ->) ->
+		if typeof(options) == "function"
+			callback = options
+			options = {}
+		DocstoreManager.getDoc project_id, doc_id, options, callback
 
 
 	addDoc: (project_or_id, folder_id, docName, docLines, sl_req_id, callback = (error, doc, folder_id) ->)=>
 	addDoc: (project_or_id, folder_id, docName, docLines, sl_req_id, callback = (error, doc, folder_id) ->)=>
 		{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
 		{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
@@ -135,6 +138,13 @@ module.exports = ProjectEntityHandler =
 							return callback(err) if err?
 							return callback(err) if err?
 							callback(null, doc, folder_id)
 							callback(null, doc, folder_id)
 
 
+	restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
+		# getDoc will return the deleted doc's lines, but we don't actually remove
+		# the deleted doc, just create a new one from its lines.
+		ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) ->
+			return callback(error) if error?
+			ProjectEntityHandler.addDoc project_id, null, name, lines, callback
+
 	addFile: (project_or_id, folder_id, fileName, path, sl_req_id, callback = (error, fileRef, folder_id) ->)->
 	addFile: (project_or_id, folder_id, fileName, path, sl_req_id, callback = (error, fileRef, folder_id) ->)->
 		{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
 		{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
 		Project.getProject project_or_id, "", (err, project) ->
 		Project.getProject project_or_id, "", (err, project) ->

+ 2 - 1
services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee

@@ -10,7 +10,8 @@ module.exports = ProjectRootDocManager =
 			root_doc_id = null
 			root_doc_id = null
 			for path, doc of docs
 			for path, doc of docs
 				for line in doc.lines || []
 				for line in doc.lines || []
-					if Path.extname(path).match(/\.R?tex$/) and line.match(/\\documentclass/)
+					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
 						root_doc_id = doc._id
 			if root_doc_id?
 			if root_doc_id?
 				ProjectEntityHandler.setRootDoc project_id, root_doc_id, sl_req_id, callback
 				ProjectEntityHandler.setRootDoc project_id, root_doc_id, sl_req_id, callback

+ 3 - 0
services/web/app/coffee/router.coffee

@@ -7,6 +7,7 @@ SpellingController = require('./Features/Spelling/SpellingController')
 SecurityManager = require('./managers/SecurityManager')
 SecurityManager = require('./managers/SecurityManager')
 AuthorizationManager = require('./Features/Security/AuthorizationManager')
 AuthorizationManager = require('./Features/Security/AuthorizationManager')
 EditorController = require("./Features/Editor/EditorController")
 EditorController = require("./Features/Editor/EditorController")
+EditorHttpController = require("./Features/Editor/EditorHttpController")
 EditorUpdatesController = require("./Features/Editor/EditorUpdatesController")
 EditorUpdatesController = require("./Features/Editor/EditorUpdatesController")
 Settings = require('settings-sharelatex')
 Settings = require('settings-sharelatex')
 TpdsController = require('./Features/ThirdPartyDataStore/TpdsController')
 TpdsController = require('./Features/ThirdPartyDataStore/TpdsController')
@@ -125,6 +126,8 @@ module.exports = class Router
 		app.get  "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
 		app.get  "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
 		app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
 		app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
 
 
+		app.post "/project/:Project_id/doc/:doc_id/restore", SecurityManager.requestCanAccessProject, EditorHttpController.restoreDoc
+
 		app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
 		app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
 		app.get  '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
 		app.get  '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
 
 

+ 14 - 2
services/web/app/views/templates.jade

@@ -82,6 +82,12 @@
 			.dropdown-caret
 			.dropdown-caret
 				i.icon-chevron-down
 				i.icon-chevron-down
 			.entity-label.label.label-success
 			.entity-label.label.label-success
+
+	script(type="text/template")#deletedDocsFolderTemplate
+		.entity-list-item.entity-deleted-docs-folder(class="entity-{{ type }}", entity-type="{{ type }}", id="{{ id }}")
+			.clickable.js-clickable
+				span.name {{ name }}
+			.entity-label.label.label-success
 	
 	
 	script(type="text/template")#entityListTemplate
 	script(type="text/template")#entityListTemplate
 		.contents
 		.contents
@@ -386,8 +392,14 @@
 
 
 	script(type='text/template')#trackChangesDiffTemplate
 	script(type='text/template')#trackChangesDiffTemplate
 		.track-changes-diff-toolbar.btn-toolbar
 		.track-changes-diff-toolbar.btn-toolbar
-			.number-of-changes {{ changes }} in <strong>{{ name }}</strong>
-			a(href="#").restore.btn.btn-small.btn-danger Restore to before these changes
+			.change-info
+				.number-of-changes {{ changes }} in <strong>{{ name }}</strong>
+				a(href="#").restore.btn.btn-small.btn-danger Restore to before these changes
+			.deleted-info(style="display:none;")
+				strong {{ name }}
+				.controls
+					span This file has been deleted 
+					a(href="#").restore-deleted.btn.btn-small.btn-success Restore
 		.track-changes-diff-editor
 		.track-changes-diff-editor
 
 
 	script(type='text/template')#changeListItemTemplate
 	script(type='text/template')#changeListItemTemplate

+ 29 - 0
services/web/public/coffee/file-tree/DeletedDocsFolderView.coffee

@@ -0,0 +1,29 @@
+define [
+	"file-tree/FolderView"
+], (FolderView) ->
+	DeletedDocsFolderView = FolderView.extend
+		template: $("#deletedDocsFolderTemplate").html()
+
+		render: () ->
+			@$el.append(Mustache.to_html @template, @model.attributes)
+			@_bindToDomElements()
+			@hideRenameBox()
+			@hideToggle()
+			@renderEntries()
+			@showEntries()
+			return @
+
+		onClick: () ->
+			e.preventDefault()
+
+		onToggle: () ->
+			e.preventDefault()
+
+		getContextMenuEntries: () -> null
+
+		hideToggle: () ->
+			@$(".js-toggle").hide()
+
+		
+
+

+ 1 - 0
services/web/public/coffee/file-tree/EntityView.coffee

@@ -112,6 +112,7 @@ define [
 
 
 		showContextMenu: (position) ->
 		showContextMenu: (position) ->
 			entries = @getContextMenuEntries()
 			entries = @getContextMenuEntries()
+			return if !entries?
 
 
 			@manager.trigger "contextmenu:beforeshow", @model, entries
 			@manager.trigger "contextmenu:beforeshow", @model, entries
 
 

+ 29 - 3
services/web/public/coffee/file-tree/FileTreeManager.coffee

@@ -3,11 +3,13 @@ define [
 	"models/File"
 	"models/File"
 	"models/Folder"
 	"models/Folder"
 	"file-tree/FileTreeView"
 	"file-tree/FileTreeView"
+	"file-tree/FolderView"
+	"file-tree/DeletedDocsFolderView"
 	"utils/Effects"
 	"utils/Effects"
 	"utils/Modal"
 	"utils/Modal"
 	"libs/backbone"
 	"libs/backbone"
 	"libs/jquery.storage"
 	"libs/jquery.storage"
-], (Doc, File, Folder, FileTreeView, Effects, Modal) ->
+], (Doc, File, Folder, FileTreeView, FolderView, DeletedDocsFolderView, Effects, Modal) ->
 	class FileTreeManager
 	class FileTreeManager
 		constructor: (@ide) ->
 		constructor: (@ide) ->
 			_.extend(@, Backbone.Events)
 			_.extend(@, Backbone.Events)
@@ -37,6 +39,13 @@ define [
 
 
 		populateFileTree: () ->
 		populateFileTree: () ->
 			@view.bindToRootFolder(@project.get("rootFolder"))
 			@view.bindToRootFolder(@project.get("rootFolder"))
+			
+			if @deletedDocsView?
+				@deletedDocsView.$el.remove()
+			@deletedDocsView = new DeletedDocsFolderView(model: @project.get("deletedDocs"), manager: @)
+			@deletedDocsView.render()
+			$("#sections").append(@deletedDocsView.$el)
+			@hideDeletedDocs()
 
 
 		listenForUpdates: () ->
 		listenForUpdates: () ->
 			@ide.socket.on 'reciveNewDoc', (folder_id, doc) =>
 			@ide.socket.on 'reciveNewDoc', (folder_id, doc) =>
@@ -100,8 +109,12 @@ define [
 			@ide.sideBarView.deselectAll()
 			@ide.sideBarView.deselectAll()
 			@views[entity_id]?.select()
 			@views[entity_id]?.select()
 
 
-		getEntity: (entity_id) ->
-			@views[entity_id]?.model
+		getEntity: (entity_id, options = {include_deleted: false}) ->
+			model = @views[entity_id]?.model
+			if !model? or (model.get("deleted") and !options.include_deleted)
+				return
+			else
+				return model
 
 
 		getSelectedEntity: () -> @getEntity(@selected_entity_id)
 		getSelectedEntity: () -> @getEntity(@selected_entity_id)
 		getSelectedEntityId: () -> @getSelectedEntity()?.id
 		getSelectedEntityId: () -> @getSelectedEntity()?.id
@@ -283,6 +296,19 @@ define [
 			entity.set("deleted", true)
 			entity.set("deleted", true)
 			entity.collection?.remove(entity)
 			entity.collection?.remove(entity)
 			delete @views[entity_id]
 			delete @views[entity_id]
+
+			# Do this after the remove so that it's never in two places at once 
+			# and so that it doesn't get reset by deleting from @views
+			if entity.get("type") == "doc"
+				@project.get("deletedDocs").get("children").add entity
 			
 			
 		setLabels: (labels) ->
 		setLabels: (labels) ->
 			@view.setLabels(labels)
 			@view.setLabels(labels)
+			@deletedDocsView.setLabels(labels)
+
+		showDeletedDocs: () ->
+			if @project.get("deletedDocs").get("children").length > 0
+				@deletedDocsView.$el.show()
+
+		hideDeletedDocs: () ->
+			@deletedDocsView.$el.hide()

+ 1 - 0
services/web/public/coffee/models/Doc.coffee

@@ -9,3 +9,4 @@ define [
 			attributes =
 			attributes =
 				id: rawAttributes._id
 				id: rawAttributes._id
 				name: rawAttributes.name
 				name: rawAttributes.name
+				deleted: !!rawAttributes.deleted

+ 3 - 3
services/web/public/coffee/models/Folder.coffee

@@ -20,11 +20,11 @@ define [
 				id: rawAttributes._id
 				id: rawAttributes._id
 				name: rawAttributes.name
 				name: rawAttributes.name
 			children = []
 			children = []
-			for childFolder in rawAttributes.folders
+			for childFolder in rawAttributes.folders or []
 				children.push new Folder(childFolder, parse: true)
 				children.push new Folder(childFolder, parse: true)
-			for file in rawAttributes.fileRefs
+			for file in rawAttributes.fileRefs or []
 				children.push new File(file, parse: true)
 				children.push new File(file, parse: true)
-			for doc in rawAttributes.docs
+			for doc in rawAttributes.docs or []
 				children.push new Doc(doc, parse: true)
 				children.push new Doc(doc, parse: true)
 			attributes.children = new FolderChildren(children)
 			attributes.children = new FolderChildren(children)
 			return attributes
 			return attributes

+ 11 - 1
services/web/public/coffee/models/Project.coffee

@@ -2,8 +2,9 @@ define [
 	"models/User"
 	"models/User"
 	"models/ProjectMemberList"
 	"models/ProjectMemberList"
 	"models/Folder"
 	"models/Folder"
+	"models/Doc"
 	"libs/backbone"
 	"libs/backbone"
-], (User, ProjectMemberList, Folder) ->
+], (User, ProjectMemberList, Folder, Doc) ->
 	Project = Backbone.Model.extend
 	Project = Backbone.Model.extend
 		initialize: ->
 		initialize: ->
 			@on "change:ide", (project, ide) =>
 			@on "change:ide", (project, ide) =>
@@ -39,6 +40,15 @@ define [
 				member = User.findOrBuild rawMember._id, rawMember
 				member = User.findOrBuild rawMember._id, rawMember
 				members.add member
 				members.add member
 
 
+			for doc in rawAttributes.deletedDocs
+				doc.deleted = true
+
+			attributes.deletedDocs = new Folder({
+				_id: "deleted-docs-folder"
+				name: "Deleted documents"
+				docs: rawAttributes.deletedDocs
+			}, parse: true)
+
 			return attributes
 			return attributes
 	
 	
 		bindToRootDocId: ->
 		bindToRootDocId: ->

+ 6 - 1
services/web/public/coffee/pdf/CompiledView.coffee

@@ -204,7 +204,12 @@ define [
 
 
 		recompilePdf: () ->
 		recompilePdf: () ->
 			@options.manager.trigger "compile:pdf"
 			@options.manager.trigger "compile:pdf"
-			@options.manager.refreshPdf()
+			rootDocOverride_id = null
+			for line in @ide.editor.getLines()
+				match = line.match /(.*)\\documentclass/
+				if match and !match[1].match /%/
+					rootDocOverride_id = @ide.editor.getCurrentDocId()
+			@options.manager.refreshPdf {rootDocOverride_id}
 
 
 		toggleFlatViewButton: () -> @$("#flatViewButton").button("toggle")
 		toggleFlatViewButton: () -> @$("#flatViewButton").button("toggle")
 		toggleSplitViewButton: () -> @$("#splitViewButton").button("toggle")
 		toggleSplitViewButton: () -> @$("#splitViewButton").button("toggle")

+ 6 - 3
services/web/public/coffee/pdf/PdfManager.coffee

@@ -143,7 +143,7 @@ define [
 				@view.onCompiling()
 				@view.onCompiling()
 				@syncButtonsView?.hide()
 				@syncButtonsView?.hide()
 				@compiling = true
 				@compiling = true
-				@_doCompile opts.isAutoCompile, (error, status, outputFiles) =>
+				@_doCompile opts, (error, status, outputFiles) =>
 					@compiling = false
 					@compiling = false
 					doneCompiling()
 					doneCompiling()
 
 
@@ -172,16 +172,19 @@ define [
 					if outputFiles?
 					if outputFiles?
 						@view.showOutputFileDownloadLinks(outputFiles)
 						@view.showOutputFileDownloadLinks(outputFiles)
 
 
-		_doCompile: (isAutoCompile, callback = (error, status, outputFiles) ->) ->
+		_doCompile: (opts, callback = (error, status, outputFiles) ->) ->
 			url = "/project/#{@ide.project_id}/compile"
 			url = "/project/#{@ide.project_id}/compile"
-			if isAutoCompile
+			if opts.isAutoCompile
 				url += "?auto_compile=true"
 				url += "?auto_compile=true"
 			$.ajax(
 			$.ajax(
 				url: url
 				url: url
 				type: "POST"
 				type: "POST"
 				headers:
 				headers:
 					"X-CSRF-Token": window.csrfToken
 					"X-CSRF-Token": window.csrfToken
+				contentType: "application/json; charset=utf-8"
 				dataType: 'json'
 				dataType: 'json'
+				data: JSON.stringify settingsOverride: 
+					rootDoc_id: opts.rootDocOverride_id ? null
 				success: (body, status, response) ->
 				success: (body, status, response) ->
 					callback null, body.status, body.outputFiles
 					callback null, body.status, body.outputFiles
 				error: (error) ->
 				error: (error) ->

+ 32 - 18
services/web/public/coffee/track-changes/DiffView.coffee

@@ -10,17 +10,23 @@ define [
 		template: $("#trackChangesDiffTemplate").html()
 		template: $("#trackChangesDiffTemplate").html()
 
 
 		events:
 		events:
-			"click .restore": () ->
-				console.log "click"
+			"click .restore": (e) ->
+				e.preventDefault()
 				@trigger "restore"
 				@trigger "restore"
+			"click .restore-deleted": (e) ->
+				e.preventDefault()
+				@$("a.restore-deleted").attr("disabled", true)
+				@$("a.restore-deleted").text("Restoring...")
+				@trigger "restore-deleted"
 
 
 		initialize: () ->
 		initialize: () ->
-			@model.on "change:diff", () => @render()
+			if !@model.get("doc").get("deleted")
+				@model.on "change:diff", () => @render()
+				@model.fetch()
+			else
+				@render()
 
 
 		render: ->
 		render: ->
-			diff = @model.get("diff")
-			return unless diff?
-
 			changes = @getNumberOfChanges()
 			changes = @getNumberOfChanges()
 			html = Mustache.to_html @template, {
 			html = Mustache.to_html @template, {
 				changes: "#{changes} change#{if changes == 1 then "" else "s"}"
 				changes: "#{changes} change#{if changes == 1 then "" else "s"}"
@@ -28,18 +34,26 @@ define [
 			}
 			}
 			@$el.html(html)
 			@$el.html(html)
 
 
-			if !@model.get("from")? or !@model.get("to")? or changes == 0
-				@$(".restore").hide()
-
-			@createAceEditor()
-			@aceEditor.setValue(@getPlainDiffContent())
-			@aceEditor.clearSelection()
-			@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
-			@insertMarkers()
-			@insertNameTag()
-			@insertMoreChangeLabels()
-			@bindToScrollEvents()
-			@scrollToFirstChange()
+			if @model.get("doc").get("deleted")
+				@$(".change-info").hide()
+				@$(".deleted-info").show()
+			else
+				diff = @model.get("diff")
+				return unless diff?
+
+				if !@model.get("from")? or !@model.get("to")? or changes == 0
+					@$(".restore").hide()
+
+				@createAceEditor()
+				@aceEditor.setValue(@getPlainDiffContent())
+				@aceEditor.clearSelection()
+				@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
+				@insertMarkers()
+				@insertNameTag()
+				@insertMoreChangeLabels()
+				@bindToScrollEvents()
+				@scrollToFirstChange()
+
 			return @
 			return @
 
 
 		remove: () ->
 		remove: () ->

+ 28 - 2
services/web/public/coffee/track-changes/TrackChangesManager.coffee

@@ -71,6 +71,9 @@ define [
 			@ide.mainAreaManager.change "trackChanges"
 			@ide.mainAreaManager.change "trackChanges"
 			@ide.editor.disable()
 			@ide.editor.disable()
 			@ide.fileViewManager.disable()
 			@ide.fileViewManager.disable()
+
+			@ide.fileTreeManager.showDeletedDocs()
+
 			@enable()
 			@enable()
 
 
 		showUpgradeView: () ->
 		showUpgradeView: () ->
@@ -93,6 +96,7 @@ define [
 			@ide.fileTreeManager.openDoc(@doc_id)
 			@ide.fileTreeManager.openDoc(@doc_id)
 			@ide.tabManager.show "code"
 			@ide.tabManager.show "code"
 			@resetLabels()
 			@resetLabels()
+			@ide.fileTreeManager.hideDeletedDocs()
 
 
 		autoSelectDiff: () ->
 		autoSelectDiff: () ->
 			if @changes.models.length == 0
 			if @changes.models.length == 0
@@ -160,7 +164,7 @@ define [
 				@diffView.remove()
 				@diffView.remove()
 
 
 			if !@diff.get("doc")?
 			if !@diff.get("doc")?
-				console.log "This document has been deleted. What should we do?"
+				console.log "This document does not exist. What should we do?"
 				return
 				return
 
 
 			@diffView = new DiffView(
 			@diffView = new DiffView(
@@ -171,7 +175,14 @@ define [
 			@diffView.on "restore", () =>
 			@diffView.on "restore", () =>
 				@restoreDiff(@diff)
 				@restoreDiff(@diff)
 
 
-			@diff.fetch()
+			@diffView.on "restore-deleted", () =>
+				@restoreDeletedDoc @diff.get("doc"), (error, doc_id) =>
+					return if error? or !doc_id?
+					setTimeout () =>
+						# Give doc a chance to appear in file tree via socket.io
+						@hide()
+						@ide.fileTreeManager.openDoc(doc_id)
+					, 1000
 
 
 			@ide.fileTreeManager.selectEntity(@doc_id)
 			@ide.fileTreeManager.selectEntity(@doc_id)
 
 
@@ -217,6 +228,21 @@ define [
 				}]
 				}]
 			})
 			})
 
 
+		restoreDeletedDoc: (doc, callback) ->
+			$.ajax {
+				url: "/project/#{@project_id}/doc/#{doc.get("id")}/restore"
+				type: "POST"
+				dataType: "json"
+				data:
+					name: doc.get("name")
+				headers:
+					"X-CSRF-Token": window.csrfToken
+				success: (body, status, response) ->
+					callback(null, body?.doc_id)
+				error: (error) ->
+					callback(error)
+			}
+
 		enable: () ->
 		enable: () ->
 			@enabled = true
 			@enabled = true
 
 

+ 1 - 1
services/web/public/coffee/track-changes/models/Change.coffee

@@ -21,6 +21,6 @@ define [
 					toV: data.toV
 					toV: data.toV
 					# TODO: We should not use a global reference here, but 
 					# TODO: We should not use a global reference here, but 
 					# it's hard to get @ide into Backbone at this point.
 					# it's hard to get @ide into Backbone at this point.
-					entity: ide.fileTreeManager.getEntity(doc_id)
+					entity: ide.fileTreeManager.getEntity(doc_id, include_deleted: true)
 
 
 			return model
 			return model

+ 1 - 1
services/web/public/coffee/track-changes/models/Diff.coffee

@@ -5,7 +5,7 @@ define [
 	Diff = Backbone.Model.extend
 	Diff = Backbone.Model.extend
 		initialize: (attributes, options) ->
 		initialize: (attributes, options) ->
 			@ide = options.ide
 			@ide = options.ide
-			@set "doc", @ide.fileTreeManager.getEntity(@get("doc_id"))
+			@set "doc", @ide.fileTreeManager.getEntity(@get("doc_id"), include_deleted: true)
 
 
 		url: () ->
 		url: () ->
 			url = "/project/#{@get("project_id")}/doc/#{@get("doc_id")}/diff"
 			url = "/project/#{@get("project_id")}/doc/#{@get("doc_id")}/diff"

+ 8 - 0
services/web/public/stylesheets/app/editor.less

@@ -414,6 +414,14 @@ body.editor {
 		}
 		}
 	}
 	}
 
 
+	.entity-deleted-docs-folder {
+		margin-top: 16px;
+		span.name {
+			padding: 6px;
+			border-bottom: 1px solid #ccc;
+		}
+	}
+
 	.entity-list {
 	.entity-list {
 		padding-left: 0px;
 		padding-left: 0px;
 	}
 	}

+ 28 - 6
services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee

@@ -24,7 +24,7 @@ describe "ClsiManager", ->
 
 
 	describe "sendRequest", ->
 	describe "sendRequest", ->
 		beforeEach ->
 		beforeEach ->
-			@ClsiManager._buildRequest = sinon.stub().callsArgWith(1, null, @request = "mock-request")
+			@ClsiManager._buildRequest = sinon.stub().callsArgWith(2, null, @request = "mock-request")
 
 
 		describe "with a successful compile", ->
 		describe "with a successful compile", ->
 			beforeEach ->
 			beforeEach ->
@@ -39,7 +39,7 @@ describe "ClsiManager", ->
 							type: "log"
 							type: "log"
 						}]
 						}]
 				})
 				})
-				@ClsiManager.sendRequest @project_id, @callback
+				@ClsiManager.sendRequest @project_id, {}, @callback
 
 
 			it "should build the request", ->
 			it "should build the request", ->
 				@ClsiManager._buildRequest
 				@ClsiManager._buildRequest
@@ -67,7 +67,7 @@ describe "ClsiManager", ->
 					compile:
 					compile:
 						status: @status = "failure"
 						status: @status = "failure"
 				})
 				})
-				@ClsiManager.sendRequest @project_id, @callback
+				@ClsiManager.sendRequest @project_id, {}, @callback
 			
 			
 			it "should call the callback with a failure statue", ->
 			it "should call the callback with a failure statue", ->
 				@callback.calledWith(null, @status).should.equal true
 				@callback.calledWith(null, @status).should.equal true
@@ -121,7 +121,7 @@ describe "ClsiManager", ->
 
 
 		describe "with a valid project", ->
 		describe "with a valid project", ->
 			beforeEach (done) ->
 			beforeEach (done) ->
-				@ClsiManager._buildRequest @project_id, (error, request) =>
+				@ClsiManager._buildRequest @project_id, null, (error, request) =>
 					@request = request
 					@request = request
 					done()
 					done()
 
 
@@ -159,10 +159,32 @@ describe "ClsiManager", ->
 						}]
 						}]
 				)
 				)
 
 
+
+		describe "when root doc override is valid", ->
+			beforeEach (done) ->
+				@ClsiManager._buildRequest @project_id, {rootDoc_id:"mock-doc-id-2"}, (error, request) =>
+					@request = request
+					done()
+
+			it "should change root path", ->
+				@request.compile.rootResourcePath.should.equal "chapters/chapter1.tex"
+
+
+		describe "when root doc override is invalid", ->
+			beforeEach (done) ->
+				@ClsiManager._buildRequest @project_id, {rootDoc_id:"invalid-id"}, (error, request) =>
+					@request = request
+					done()
+
+			it "should fallback to default root doc", ->
+				@request.compile.rootResourcePath.should.equal "main.tex"
+
+
+
 		describe "when the project has an invalid compiler", ->
 		describe "when the project has an invalid compiler", ->
 			beforeEach (done) ->
 			beforeEach (done) ->
 				@project.compiler = "context"
 				@project.compiler = "context"
-				@ClsiManager._buildRequest @project, (error, request) =>
+				@ClsiManager._buildRequest @project, null, (error, request) =>
 					@request = request
 					@request = request
 					done()
 					done()
 
 
@@ -172,7 +194,7 @@ describe "ClsiManager", ->
 		describe "when there is no valid root document", ->
 		describe "when there is no valid root document", ->
 			beforeEach (done) ->
 			beforeEach (done) ->
 				@project.rootDoc_id = "not-valid"
 				@project.rootDoc_id = "not-valid"
-				@ClsiManager._buildRequest @project, (@error, @request) =>
+				@ClsiManager._buildRequest @project, null, (@error, @request) =>
 					done()
 					done()
 			
 			
 			it "should return an error", ->
 			it "should return an error", ->

+ 2 - 2
services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee

@@ -44,7 +44,7 @@ describe "CompileController", ->
 
 
 			it "should do the compile without the auto compile flag", ->
 			it "should do the compile without the auto compile flag", ->
 				@CompileManager.compile
 				@CompileManager.compile
-					.calledWith(@project_id, @user_id, { isAutoCompile: false })
+					.calledWith(@project_id, @user_id, { isAutoCompile: false, settingsOverride:{} })
 					.should.equal true
 					.should.equal true
 
 
 			it "should set the content-type of the response to application/json", ->
 			it "should set the content-type of the response to application/json", ->
@@ -71,7 +71,7 @@ describe "CompileController", ->
 
 
 			it "should do the compile with the auto compile flag", ->
 			it "should do the compile with the auto compile flag", ->
 				@CompileManager.compile
 				@CompileManager.compile
-					.calledWith(@project_id, @user_id, { isAutoCompile: true })
+					.calledWith(@project_id, @user_id, { isAutoCompile: true, settingsOverride:{} })
 					.should.equal true
 					.should.equal true
 
 
 	describe "downloadPdf", ->
 	describe "downloadPdf", ->

+ 1 - 1
services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee

@@ -36,7 +36,7 @@ describe "CompileManager", ->
 			@CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, false)
 			@CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, false)
 			@CompileManager._ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null)
 			@CompileManager._ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null)
 			@DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null)
 			@DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null)
-			@ClsiManager.sendRequest = sinon.stub().callsArgWith(1, null, @status = "mock-status")
+			@ClsiManager.sendRequest = sinon.stub().callsArgWith(2, null, @status = "mock-status")
 
 
 		describe "succesfully", ->
 		describe "succesfully", ->
 			beforeEach ->
 			beforeEach ->

+ 16 - 0
services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee

@@ -131,6 +131,22 @@ describe "DocstoreManager", ->
 					}, "error getting doc from docstore")
 					}, "error getting doc from docstore")
 					.should.equal true
 					.should.equal true
 
 
+		describe "with include_deleted=true", ->
+			beforeEach ->
+				@request.get = sinon.stub().callsArgWith(1, null, statusCode: 204, @doc)
+				@DocstoreManager.getDoc @project_id, @doc_id, include_deleted: true, @callback
+
+			it "should get the doc from the docstore api (including deleted)", ->
+				@request.get
+					.calledWith({
+						url: "#{@settings.apis.docstore.url}/project/#{@project_id}/doc/#{@doc_id}?include_deleted=true"
+						json: true
+					})
+					.should.equal true
+
+			it "should call the callback with the lines, version and rev", ->
+				@callback.calledWith(null, @lines, @rev).should.equal true
+
 	describe "getAllDocs", ->
 	describe "getAllDocs", ->
 		describe "with a successful response code", ->
 		describe "with a successful response code", ->
 			beforeEach ->
 			beforeEach ->

+ 46 - 0
services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee

@@ -0,0 +1,46 @@
+SandboxedModule = require('sandboxed-module')
+sinon = require('sinon')
+require('chai').should()
+modulePath = require('path').join __dirname, '../../../../app/js/Features/Editor/EditorHttpController'
+
+describe "EditorHttpController", ->
+	beforeEach ->
+		@EditorHttpController = SandboxedModule.require modulePath, requires:
+			'../Project/ProjectEntityHandler' : @ProjectEntityHandler = {}
+			"./EditorRealTimeController": @EditorRealTimeController = {}
+			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
+		@project_id = "mock-project-id"
+		@doc_id = "mock-doc-id"
+		@req = {}
+		@res =
+			send: sinon.stub()
+			json: sinon.stub()
+
+	describe "restoreDoc", ->
+		beforeEach ->
+			@req.params =
+				Project_id: @project_id
+				doc_id: @doc_id
+			@req.body =
+				name: @name = "doc-name"
+			@ProjectEntityHandler.restoreDoc = sinon.stub().callsArgWith(3, null,
+				@doc = { "mock": "doc", _id: @new_doc_id = "new-doc-id" }
+				@folder_id = "mock-folder-id"
+			)
+			@EditorRealTimeController.emitToRoom = sinon.stub()
+			@EditorHttpController.restoreDoc @req, @res
+
+		it "should restore the doc", ->
+			@ProjectEntityHandler.restoreDoc
+				.calledWith(@project_id, @doc_id, @name)
+				.should.equal true
+
+		it "should the real-time clients about the new doc", ->
+			@EditorRealTimeController.emitToRoom
+				.calledWith(@project_id, 'reciveNewDoc', @folder_id, @doc)
+				.should.equal true
+
+		it "should return the new doc id", ->
+			@res.json
+				.calledWith(doc_id: @new_doc_id)
+				.should.equal true

+ 8 - 0
services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee

@@ -55,6 +55,10 @@ describe "ProjectEditorHandler", ->
 				last_name  : "Write"
 				last_name  : "Write"
 				email      : "read-write@sharelatex.com"
 				email      : "read-write@sharelatex.com"
 			}]
 			}]
+			deletedDocs: [{
+				_id: "deleted-doc-id"
+				name: "main.tex"
+			}]
 		@handler = SandboxedModule.require modulePath
 		@handler = SandboxedModule.require modulePath
 
 
 	describe "buildProjectModelView", ->
 	describe "buildProjectModelView", ->
@@ -85,6 +89,10 @@ describe "ProjectEditorHandler", ->
 				@result.owner.first_name.should.equal "Owner"
 				@result.owner.first_name.should.equal "Owner"
 				@result.owner.last_name.should.equal "ShareLaTeX"
 				@result.owner.last_name.should.equal "ShareLaTeX"
 				@result.owner.privileges.should.equal "owner"
 				@result.owner.privileges.should.equal "owner"
+
+			it "should include the deletedDocs", ->
+				should.exist @result.deletedDocs
+				@result.deletedDocs.should.equal @project.deletedDocs
 				
 				
 			it "should gather readOnly_refs and collaberators_refs into a list of members", ->
 			it "should gather readOnly_refs and collaberators_refs into a list of members", ->
 				findMember = (id) =>
 				findMember = (id) =>

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

@@ -286,7 +286,7 @@ describe 'ProjectEntityHandler', ->
 		beforeEach ->
 		beforeEach ->
 			@lines = ["mock", "doc", "lines"]
 			@lines = ["mock", "doc", "lines"]
 			@rev = 5
 			@rev = 5
-			@DocstoreManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @rev)
+			@DocstoreManager.getDoc = sinon.stub().callsArgWith(3, null, @lines, @rev)
 			@ProjectEntityHandler.getDoc project_id, doc_id, @callback
 			@ProjectEntityHandler.getDoc project_id, doc_id, @callback
 
 
 		it "should call the docstore", ->
 		it "should call the docstore", ->
@@ -339,6 +339,31 @@ describe 'ProjectEntityHandler', ->
 				.calledWith(project_id, @doc._id.toString(), @lines)
 				.calledWith(project_id, @doc._id.toString(), @lines)
 				.should.equal true
 				.should.equal true
 
 
+	describe "restoreDoc", ->
+		beforeEach ->
+			@name = "doc-name"
+			@lines = ['1234','abc']
+			@doc = { "mock": "doc" }
+			@folder_id = "mock-folder-id"
+			@callback = sinon.stub()
+			@ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(3, null, @lines)
+			@ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(4, null, @doc, @folder_id)
+
+			@ProjectEntityHandler.restoreDoc project_id, doc_id, @name, @callback
+
+		it 'should get the doc lines', ->
+			@ProjectEntityHandler.getDoc
+				.calledWith(project_id, doc_id, include_deleted: true)
+				.should.equal true
+
+		it "should add a new doc with these doc lines", ->
+			@ProjectEntityHandler.addDoc
+				.calledWith(project_id, null, @name, @lines)
+				.should.equal true
+
+		it "should call the callback with the new folder and doc", ->
+			@callback.calledWith(null, @doc, @folder_id).should.equal true
+
 	describe 'adding file', ->
 	describe 'adding file', ->
 		fileName = "something.jpg"
 		fileName = "something.jpg"
 		beforeEach ->
 		beforeEach ->