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

Merge branch 'master' into sk-session-revocation

Shane Kilkelly 10 лет назад
Родитель
Сommit
fc6cf75ad5
27 измененных файлов с 245 добавлено и 77 удалено
  1. 23 13
      services/web/Gruntfile.coffee
  2. 8 0
      services/web/README.md
  3. 1 1
      services/web/app/coffee/Features/Blog/BlogController.coffee
  4. 19 2
      services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
  5. 4 8
      services/web/app/coffee/Features/Compile/CompileController.coffee
  6. 1 1
      services/web/app/coffee/Features/Compile/CompileManager.coffee
  7. 4 0
      services/web/app/coffee/Features/Email/EmailBuilder.coffee
  8. 5 5
      services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
  9. 5 3
      services/web/app/coffee/Features/Project/ProjectDeleter.coffee
  10. 3 3
      services/web/app/views/layout/footer.jade
  11. 2 2
      services/web/app/views/project/editor/pdf.jade
  12. 4 0
      services/web/config/settings.defaults.coffee
  13. 0 1
      services/web/package.json
  14. 2 1
      services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
  15. 8 11
      services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
  16. 2 1
      services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
  17. 9 1
      services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee
  18. 1 5
      services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee
  19. BIN
      services/web/public/img/sprite.png
  20. 105 0
      services/web/public/stylesheets/app/sprites.less
  21. 3 8
      services/web/public/stylesheets/app/wiki.less
  22. 5 0
      services/web/public/stylesheets/components/footer.less
  23. 1 0
      services/web/public/stylesheets/style.less
  24. 19 4
      services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
  25. 1 5
      services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
  26. 1 1
      services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee
  27. 9 1
      services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee

+ 23 - 13
services/web/Gruntfile.coffee

@@ -17,8 +17,9 @@ module.exports = (grunt) ->
 	grunt.loadNpmTasks 'grunt-contrib-watch'
 	grunt.loadNpmTasks 'grunt-parallel'
 	grunt.loadNpmTasks 'grunt-exec'
-	grunt.loadNpmTasks 'grunt-contrib-imagemin'
 	grunt.loadNpmTasks 'grunt-contrib-cssmin'
+	# grunt.loadNpmTasks 'grunt-contrib-imagemin'
+	# grunt.loadNpmTasks 'grunt-sprity'
 
 	config =
 
@@ -47,18 +48,26 @@ module.exports = (grunt) ->
 					stream:true
 
 
-		imagemin:
-			dynamic:                       
-				files: [{
-					expand: true
-					cwd: 'public/img/'
-					src: ['**/*.{png,jpg,gif}']
-					dest: 'public/img/'
-				}]
-			options:
-				interlaced:false
-				optimizationLevel: 7
-
+		# imagemin:
+		# 	dynamic:                       
+		# 		files: [{
+		# 			expand: true
+		# 			cwd: 'public/img/'
+		# 			src: ['**/*.{png,jpg,gif}']
+		# 			dest: 'public/img/'
+		# 		}]
+		# 	options:
+		# 		interlaced:false
+		# 		optimizationLevel: 7
+
+		# sprity:
+		# 	sprite:
+		# 		options:
+		# 			cssPath:"/img/"
+		# 			'style': '../../public/stylesheets/app/sprites.less'
+		# 			margin: 0
+		# 		src: ['./public/img/flags/24/*.png']
+		# 		dest: './public/img/sprite'
 
 
 		coffee:
@@ -218,6 +227,7 @@ module.exports = (grunt) ->
 				pattern: "@@RELEASE@@"
 				replacement: process.env.BUILD_NUMBER || "(unknown build)"
 
+
 			
 
 		availabletasks:

+ 8 - 0
services/web/README.md

@@ -6,9 +6,17 @@ web-sharelatex is the front-end web service of the open-source web-based collabo
 It serves all the HTML pages, CSS and javascript to the client. web-sharelatex also contains 
 a lot of logic around creating and editing projects, and account management.
 
+
 The rest of the ShareLaTeX stack, along with information about contributing can be found in the 
 [sharelatex/sharelatex](https://github.com/sharelatex/sharelatex) repository.
 
+Build process
+----------------
+
+web-sharelatex uses [Grunt](http://gruntjs.com/) to build its front-end related assets.
+
+Image processing tasks are commented out in the gruntfile and the needed packages aren't presently in the project's `package.json`. If the images need to be processed again (minified and sprited), start by fetching the packages (`npm install grunt-contrib-imagemin grunt-sprity`), then *decomment* the tasks in `Gruntfile.coffee`. After this, the tasks can be called (explicitly, via `grunt imagemin` and `grunt sprity`).
+
 Unit test status
 ----------------
 

+ 1 - 1
services/web/app/coffee/Features/Blog/BlogController.coffee

@@ -20,7 +20,7 @@ module.exports = BlogController =
 
 		logger.log url:url, "proxying request to blog api"
 		request.get blogUrl, (err, r, data)->
-			if r?.statusCode == 404
+			if r?.statusCode == 404 or r?.statusCode == 403
 				return ErrorController.notFound(req, res, next)
 			if err?
 				return res.send 500

+ 19 - 2
services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee

@@ -30,12 +30,17 @@ module.exports = CollaboratorsHandler =
 	getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
 		CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
 			return callback(error) if error?
+			result = []
 			async.mapLimit members, 3,
 				(member, cb) ->
 					UserGetter.getUser member.id, (error, user) ->
 						return cb(error) if error?
-						return cb(null, { user: user, privilegeLevel: member.privilegeLevel })
-				callback
+						if user?
+							result.push { user: user, privilegeLevel: member.privilegeLevel }
+						cb()
+				(error) ->
+					return callback(error) if error?
+					callback null, result
 	
 	getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
 		# In future if the schema changes and getting all member ids is more expensive (multiple documents)
@@ -82,6 +87,18 @@ module.exports = CollaboratorsHandler =
 				logger.error err: err, "problem removing user from project collaberators"
 			callback(err)
 	
+	removeUserFromAllProjets: (user_id, callback = (error) ->) ->
+		CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) ->
+			return callback(error) if error?
+			allProjects = readAndWriteProjects.concat(readOnlyProjects)
+			jobs = []
+			for project in allProjects
+				do (project) ->
+					jobs.push (cb) ->
+						return cb() if !project?
+						CollaboratorsHandler.removeUserFromProject project._id, user_id, cb
+			async.series jobs, callback
+	
 	addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
 		emails = mimelib.parseAddresses(unparsed_email)
 		email = emails[0]?.address?.toLowerCase()

+ 4 - 8
services/web/app/coffee/Features/Compile/CompileController.coffee

@@ -29,8 +29,6 @@ module.exports = CompileController =
 				options.compiler = req.body.compiler
 			if req.body?.draft
 				options.draft = req.body.draft
-			if req.query?.isolated is "true"
-				options.isolated = true
 			logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
 			CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
 				return next(error) if error?
@@ -44,17 +42,15 @@ module.exports = CompileController =
 				}
 
 	_compileAsUser: (req, callback) ->
-		# callback with user_id if isolated flag is set on request, undefined otherwise
-		isolated = req.query?.isolated is "true"
-		if isolated
+		# callback with user_id if per-user, undefined otherwise
+		if not Settings.disablePerUserCompiles
 			AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
 		else
 			callback() # do a per-project compile, not per-user
 
 	_downloadAsUser: (req, callback) ->
-		# callback with user_id if isolated flag or user_id param is set on request, undefined otherwise
-		isolated = req.query?.isolated is "true" or req.params.user_id?
-		if isolated
+		# callback with user_id if per-user, undefined otherwise
+		if not Settings.disablePerUserCompiles
 			AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
 		else
 			callback() # do a per-project compile, not per-user

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

@@ -38,7 +38,7 @@ module.exports = CompileManager =
 							for key, value of limits
 								options[key] = value
 							# only pass user_id down to clsi if this is a per-user compile
-							compileAsUser = if options.isolated then user_id else undefined
+							compileAsUser = if Settings.disablePerUserCompiles then undefined else user_id
 							ClsiManager.sendRequest project_id, compileAsUser, options, (error, status, outputFiles, clsiServerId, validationProblems) ->
 								return callback(error) if error?
 								logger.log files: outputFiles, "output files"

+ 4 - 0
services/web/app/coffee/Features/Email/EmailBuilder.coffee

@@ -3,6 +3,8 @@ PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
 NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
 settings = require("settings-sharelatex")
 
+
+
 templates = {}
 
 templates.registered =	
@@ -114,6 +116,8 @@ module.exports =
 		template = templates[templateName]
 		opts.siteUrl = settings.siteUrl
 		opts.body = template.compiledTemplate(opts)
+		if settings.email?.templates?.customFooter?
+			opts.body += settings.email?.templates?.customFooter
 		return {
 			subject : template.subject(opts)
 			html: template.layout(opts)

+ 5 - 5
services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee

@@ -6,7 +6,7 @@ oneSecond = 1000
 
 makeRequest = (opts, callback)->
 	if !settings.apis.notifications?.url?
-		return callback()
+		return callback(null, statusCode:200)
 	else
 		request(opts, callback)
 
@@ -18,7 +18,7 @@ module.exports =
 			json: true
 			timeout: oneSecond
 			method: "GET"
-		request opts, (err, res, unreadNotifications)->
+		makeRequest opts, (err, res, unreadNotifications)->
 			statusCode =  if res? then res.statusCode else 500
 			if err? or statusCode != 200
 				e = new Error("something went wrong getting notifications, #{err}, #{statusCode}")
@@ -40,7 +40,7 @@ module.exports =
 				templateKey:templateKey
 			}
 		logger.log opts:opts, "creating notification for user"
-		request opts, callback
+		makeRequest opts, callback
 
 	markAsReadWithKey: (user_id, key, callback)->
 		opts = 
@@ -51,7 +51,7 @@ module.exports =
 				key:key
 			}
 		logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
-		request opts, callback
+		makeRequest opts, callback
 	
 
 	markAsRead: (user_id, notification_id, callback)->
@@ -60,4 +60,4 @@ module.exports =
 			uri: "#{settings.apis.notifications?.url}/user/#{user_id}/notification/#{notification_id}"
 			timeout:oneSecond
 		logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
-		request opts, callback
+		makeRequest opts, callback

+ 5 - 3
services/web/app/coffee/Features/Project/ProjectDeleter.coffee

@@ -24,9 +24,11 @@ module.exports = ProjectDeleter =
 		update = {deletedByExternalDataSource: false}
 		Project.update conditions, update, {}, callback
 
-	deleteUsersProjects: (owner_id, callback)->
-		logger.log owner_id:owner_id, "deleting users projects"
-		Project.remove owner_ref:owner_id, callback
+	deleteUsersProjects: (user_id, callback)->
+		logger.log {user_id}, "deleting users projects"
+		Project.remove owner_ref:user_id, (error) ->
+			return callback(error) if error?
+			CollaboratorsHandler.removeUserFromAllProjets user_id, callback
 
 	deleteProject: (project_id, callback = (error) ->) ->
 		# archiveProject takes care of the clean-up

+ 3 - 3
services/web/app/views/layout/footer.jade

@@ -15,7 +15,7 @@ footer.site-footer
 							aria-expanded="false",
 							tooltip="#{translate('language')}"
 						)
-							img(src="/img/flags/24/#{currentLngCode}.png")
+							figure(class="sprite-icon sprite-icon-lang sprite-icon-#{currentLngCode}")
 
 						ul.dropdown-menu(role="menu")
 							li.dropdown-header #{translate("language")}
@@ -23,9 +23,9 @@ footer.site-footer
 								if !subdomainDetails.hide
 									li.lngOption
 										a.menu-indent(href=subdomainDetails.url+currentUrl)
-											img(src="/img/flags/24/#{subdomainDetails.lngCode}.png")
+											figure(class="sprite-icon sprite-icon-lang sprite-icon-#{subdomainDetails.lngCode}")
 											|  #{translate(subdomainDetails.lngCode)}
-										
+										//- img(src="/img/flags/24/.png")
 				each item in nav.left_footer
 					li
 						if item.url

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

@@ -128,12 +128,12 @@ div.full-size.pdf(ng-controller="PdfController")
 						)
 							label.card-hint-feedback-label #{translate("log_hint_feedback_label")}
 							a.card-hint-feedback-positive(
-								ng-click="feedbackSent = true;"
+								ng-click="trackLogHintsPositiveFeedback(entry.ruleId); feedbackSent = true;"
 								href
 							) #{translate("answer_yes")}
 							span  / 
 							a.card-hint-feedback-negative(
-								ng-click="feedbackSent = true;"
+								ng-click="trackLogHintsNegativeFeedback(entry.ruleId); feedbackSent = true;"
 								href
 							) #{translate("answer_no")}
 						.card-hint-feedback(ng-show="feedbackSent")

+ 4 - 0
services/web/config/settings.defaults.coffee

@@ -262,6 +262,10 @@ module.exports = settings =
 	# Should we allow access to any page without logging in? This includes
 	# public projects, /learn, /templates, about pages, etc.
 	allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
+
+	# Use a single compile directory for all users in a project
+	# (otherwise each user has their own directory)
+	# disablePerUserCompiles: true
 	
 	# Maximum size of text documents in the real-time editing system.
 	max_doc_length: 2 * 1024 * 1024 # 2mb

+ 0 - 1
services/web/package.json

@@ -70,7 +70,6 @@
     "grunt-contrib-clean": "0.5.0",
     "grunt-contrib-coffee": "0.10.0",
     "grunt-contrib-cssmin": "^1.0.1",
-    "grunt-contrib-imagemin": "^1.0.1",
     "grunt-contrib-less": "0.9.0",
     "grunt-contrib-requirejs": "0.4.1",
     "grunt-contrib-watch": "^1.0.0",

+ 2 - 1
services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee

@@ -12,7 +12,8 @@ define [
 			ruleDetails = _getRule entry.message
 
 			if (ruleDetails?)
-				entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() if ruleDetails.regexToMatch?
+				entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.regexToMatch?
+				
 				entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
 				entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?
 	

+ 8 - 11
services/web/public/coffee/ide/pdf/controllers/PdfController.coffee

@@ -15,6 +15,13 @@ define [
 		$scope.shouldShowLogs = false
 		$scope.wikiEnabled = window.wikiEnabled;
 
+		# log hints tracking
+		trackLogHintsFeedback = (isPositive, hintId) ->
+			event_tracking.send 'log-hints', (if isPositive then 'feedback-positive' else 'feedback-negative'), hintId
+
+		$scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId
+		$scope.trackLogHintsNegativeFeedback = (hintId) -> trackLogHintsFeedback false, hintId
+
 		if ace.require("ace/lib/useragent").isMac
 			$scope.modifierKey = "Cmd"
 		else
@@ -50,8 +57,6 @@ define [
 			params = {}
 			if options.isAutoCompile
 				params["auto_compile"]=true
-			if perUserCompile # send ?isolated=true for per-user compiles
-				params["isolated"] = true
 			return $http.post url, {
 				rootDoc_id: options.rootDocOverride_id or null
 				draft: $scope.draft
@@ -125,9 +130,6 @@ define [
 				# convert the qs hash into a query string and append it
 				$scope.pdf.qs = createQueryString qs
 				$scope.pdf.url += $scope.pdf.qs
-				# special case for the download url
-				if perUserCompile
-					qs.isolated = true
 				# Save all downloads as files
 				qs.popupDownload = true
 				$scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs)
@@ -147,8 +149,6 @@ define [
 					else
 						file.name = file.path
 					qs = {}
-					if perUserCompile
-						qs.isolated = true
 					if response.clsiServerId?
 						qs.clsiserverid = response.clsiServerId
 					file.url = "/project/#{project_id}/output/#{file.path}" +	createQueryString qs
@@ -237,7 +237,7 @@ define [
 			return null
 
 		normalizeFilePath = (path) ->
-			path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
+			path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}(-[0-9a-f]{24})?\/(\.\/)?/, "")
 			path = path.replace(/^\/compile\//, "")
 
 			rootDocDirname = ide.fileTreeManager.getRootDocDirname()
@@ -274,7 +274,6 @@ define [
 				method: "DELETE"
 				params:
 					clsiserverid:ide.clsiServerId
-					isolated: perUserCompile
 				headers:
 					"X-Csrf-Token": window.csrfToken
 			}
@@ -361,7 +360,6 @@ define [
 							line: row + 1
 							column: column
 							clsiserverid:ide.clsiServerId
-							isolated: perUserCompile
 						}
 					})
 					.success (data) ->
@@ -407,7 +405,6 @@ define [
 							h: h.toFixed(2)
 							v: v.toFixed(2)
 							clsiserverid:ide.clsiServerId
-							isolated: perUserCompile
 						}
 					})
 					.success (data) ->

+ 2 - 1
services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee

@@ -12,7 +12,8 @@ define [
 
 			constructor: (@url, @options) ->
 				# PDFJS.disableFontFace = true  # avoids repaints, uses worker more
-				# PDFJS.disableAutoFetch = true # enable this to prevent loading whole file
+				if @options.disableAutoFetch
+					PDFJS.disableAutoFetch = true # prevent loading whole file
 				# PDFJS.disableStream
 				# PDFJS.disableRange
 				@scale = @options.scale || 1

+ 9 - 1
services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee

@@ -27,13 +27,21 @@ define [
 			$scope.document.destroy() if $scope.document?
 			$scope.loadCount = if $scope.loadCount? then $scope.loadCount + 1 else 1
 			# TODO need a proper url manipulation library to add to query string
-			$scope.document = new PDFRenderer($scope.pdfSrc + '&pdfng=true' , {
+			url = $scope.pdfSrc
+			# add 'pdfng=true' to show that we are using the angular pdfjs viewer
+			queryStringExists = url.match(/\?/)
+			url = url + (if not queryStringExists then '?' else '&') + 'pdfng=true'
+			# for isolated compiles, load the pdf on-demand because nobody will overwrite it
+			onDemandLoading = window.location?.search?.match(/isolated=true/)?
+			$scope.document = new PDFRenderer(url, {
 				scale: 1,
+				disableAutoFetch: if onDemandLoading then true else undefined
 				navigateFn: (ref) ->
 					# this function captures clicks on the annotation links
 					$scope.navigateTo = ref
 					$scope.$apply()
 				progressCallback: (progress) ->
+					return if onDemandLoading is true # don't show progress for on-demand page loading
 					$scope.$emit 'progress', progress
 				loadedCallback: () ->
 					$scope.$emit 'loaded'

+ 1 - 5
services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee

@@ -5,15 +5,11 @@ define [
 		$scope.status = 
 			loading:true
 
-		# enable per-user containers by default
-		perUserCompile = true
-
 		opts =
 			url:"/project/#{ide.project_id}/wordcount"
 			method:"GET"
 			params:
 				clsiserverid:ide.clsiServerId
-				isolated: perUserCompile
 		$http opts
 			.success (data) ->
 				$scope.status.loading = false
@@ -22,4 +18,4 @@ define [
 				$scope.status.error = true
 
 		$scope.cancel = () ->
-			$modalInstance.dismiss('cancel')
+			$modalInstance.dismiss('cancel')

BIN
services/web/public/img/sprite.png


+ 105 - 0
services/web/public/stylesheets/app/sprites.less

@@ -0,0 +1,105 @@
+
+.sprite-icon {
+  background-image: url('/img/sprite.png');
+}
+
+.sprite-icon-ko {
+  background-position: -0px -0px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-cn {
+  background-position: -0px -24px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-da {
+  background-position: -0px -48px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-de {
+  background-position: -0px -72px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-en {
+  background-position: -0px -96px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-es {
+  background-position: -0px -120px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-fi {
+  background-position: -0px -144px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-fr {
+  background-position: -0px -168px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-it {
+  background-position: -0px -192px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-ja {
+  background-position: -0px -216px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-cs {
+  background-position: -0px -240px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-nl {
+  background-position: -0px -264px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-no {
+  background-position: -0px -288px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-pl {
+  background-position: -0px -312px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-pt {
+  background-position: -0px -336px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-ru {
+  background-position: -0px -360px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-sv {
+  background-position: -0px -384px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-tr {
+  background-position: -0px -408px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-uk {
+  background-position: -0px -432px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-zh-CN {
+  background-position: -0px -456px;
+  width: 24px;
+  height: 24px;
+}

+ 3 - 8
services/web/public/stylesheets/app/wiki.less

@@ -41,11 +41,6 @@
 	.example {
 		max-width: 100%;
 
-		& > div {
-			display: block !important;
-			width: auto !important;
-		}
-
 		.code {
 			pre {
 				background-color: @gray-lightest;
@@ -60,9 +55,9 @@
 			padding-top: 10px;
 			
 			img {
-				width: auto !important;
-				height: auto !important;
-				max-width: 100% !important;
+				width: auto;
+				height: auto;
+				max-width: 100%;
 				box-shadow: 0 1px 3px @gray-light;
 				border-radius: 6px;
 			}

+ 5 - 0
services/web/public/stylesheets/components/footer.less

@@ -23,3 +23,8 @@ footer.site-footer {
 		}
 	}
 }
+
+.sprite-icon-lang {
+	display: inline-block;
+	vertical-align: middle;
+}

+ 1 - 0
services/web/public/stylesheets/style.less

@@ -73,3 +73,4 @@
 @import "app/wiki.less";
 @import "app/translations.less";
 @import "app/contact-us.less";
+@import "app/sprites.less";

+ 19 - 4
services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee

@@ -77,17 +77,19 @@ describe "CollaboratorsHandler", ->
 				{ id: "read-only-ref-2", privilegeLevel: "readOnly" }
 				{ id: "read-write-ref-1", privilegeLevel: "readAndWrite" }
 				{ id: "read-write-ref-2", privilegeLevel: "readAndWrite" }
+				{ id: "doesnt-exist", privilegeLevel: "readAndWrite" }
 			])
 			@UserGetter.getUser = sinon.stub()
 			@UserGetter.getUser.withArgs("read-only-ref-1").yields(null, { _id: "read-only-ref-1" })
 			@UserGetter.getUser.withArgs("read-only-ref-2").yields(null, { _id: "read-only-ref-2" })
 			@UserGetter.getUser.withArgs("read-write-ref-1").yields(null, { _id: "read-write-ref-1" })
 			@UserGetter.getUser.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" })
+			@UserGetter.getUser.withArgs("doesnt-exist").yields(null, null)
 			@CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback
 		
 		it "should return an array of members with their privilege levels", ->
 			@callback
-				.calledWith(undefined, [
+				.calledWith(null, [
 					{ user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" }
 					{ user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" }
 					{ user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite" }
@@ -274,6 +276,19 @@ describe "CollaboratorsHandler", ->
 			it "should not add any users to the proejct", ->
 				@CollaboratorHandler.addUserIdToProject.called.should.equal false
 
-
-
-
+	describe "removeUserFromAllProjects", ->
+		beforeEach (done) ->
+			@CollaboratorHandler.getProjectsUserIsCollaboratorOf = sinon.stub()
+			@CollaboratorHandler.getProjectsUserIsCollaboratorOf.withArgs(@user_id, { _id: 1 }).yields(
+				null,
+				[ { _id: "read-and-write-0" }, { _id: "read-and-write-1" }, null ],
+				[ { _id: "read-only-0" }, { _id: "read-only-1" }, null ]
+			)
+			@CollaboratorHandler.removeUserFromProject = sinon.stub().yields()
+			@CollaboratorHandler.removeUserFromAllProjets @user_id, done
+		
+		it "should remove the user from each project", ->
+			for project_id in ["read-and-write-0", "read-and-write-1", "read-only-0", "read-only-1"]
+				@CollaboratorHandler.removeUserFromProject
+					.calledWith(project_id, @user_id)
+					.should.equal true

+ 1 - 5
services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee

@@ -139,7 +139,7 @@ describe "CompileController", ->
 					.should.equal true
 
 			it "should proxy the PDF from the CLSI", ->
-				@CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/output/output.pdf", @req, @res, @next).should.equal true
+				@CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/user/#{@user_id}/output/output.pdf", @req, @res, @next).should.equal true
 
 		describe "when the pdf is not going to be used in pdfjs viewer", ->
 
@@ -338,8 +338,6 @@ describe "CompileController", ->
 			@req =
 				params:
 					project_id:@project_id
-				query:
-					isolated: "true"
 			@CompileManager.compile.callsArgWith(3)
 			@CompileController.proxyToClsi = sinon.stub()
 			@res = 
@@ -362,8 +360,6 @@ describe "CompileController", ->
 			@CompileManager.wordCount = sinon.stub().callsArgWith(3, null, {content:"body"})
 			@req.params =
 				Project_id: @project_id
-			@req.query =
-				isolated: "true"
 			@res.send = sinon.stub()
 			@res.contentType = sinon.stub()
 			@CompileController.wordCount @req, @res, @next

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

@@ -71,7 +71,7 @@ describe "CompileManager", ->
 
 			it "should run the compile with the compile limits", ->
 				@ClsiManager.sendRequest
-					.calledWith(@project_id, undefined, {
+					.calledWith(@project_id, @user_id, {
 						timeout: @limits.timeout
 					})
 					.should.equal true

+ 9 - 1
services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee

@@ -27,13 +27,15 @@ describe 'ProjectDeleter', ->
 			removeProjectFromAllTags: sinon.stub().callsArgWith(2)
 		@ProjectGetter =
 			getProject:sinon.stub()
+		@CollaboratorsHandler =
+			removeUserFromAllProjets: sinon.stub().yields()
 		@deleter = SandboxedModule.require modulePath, requires:
 			"../Editor/EditorController": @editorController
 			'../../models/Project':{Project:@Project}
 			'../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler
 			"../Tags/TagsHandler":@TagsHandler
 			"../FileStore/FileStoreHandler": @FileStoreHandler = {}
-			"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
+			"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
 			"./ProjectGetter": @ProjectGetter
 			'logger-sharelatex':
 				log:->
@@ -74,6 +76,12 @@ describe 'ProjectDeleter', ->
 				@Project.remove.calledWith(owner_ref:user_id).should.equal true
 				done()
 
+		it "should remove all the projects the user is a collaborator of", (done)->
+			user_id = 1234
+			@deleter.deleteUsersProjects user_id, =>
+				@CollaboratorsHandler.removeUserFromAllProjets.calledWith(user_id).should.equal true
+				done()
+
 	describe "deleteProject", ->
 		beforeEach (done) ->
 			@project_id = "mock-project-id-123"