diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 1fae80dd90..78ce4190d0 100644 --- a/services/web/Gruntfile.coffee +++ b/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: diff --git a/services/web/README.md b/services/web/README.md index 9d69f8744d..f777e7e5f5 100644 --- a/services/web/README.md +++ b/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 ---------------- diff --git a/services/web/app/coffee/Features/Blog/BlogController.coffee b/services/web/app/coffee/Features/Blog/BlogController.coffee index 8f726602c3..11af432624 100644 --- a/services/web/app/coffee/Features/Blog/BlogController.coffee +++ b/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 diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 71737eecff..81557fea42 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/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() diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 5f3edd4de5..a790948db5 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/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 diff --git a/services/web/app/coffee/Features/Compile/CompileManager.coffee b/services/web/app/coffee/Features/Compile/CompileManager.coffee index 8350ca1196..c561576525 100755 --- a/services/web/app/coffee/Features/Compile/CompileManager.coffee +++ b/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" diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index e14b9e4582..4bcf0c671d 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/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) diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee index d3e2cb3cb9..a7cf6a4672 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ b/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 diff --git a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee index 8ba8a65845..f398ea75b5 100644 --- a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee +++ b/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 diff --git a/services/web/app/views/layout/footer.jade b/services/web/app/views/layout/footer.jade index 7ef26b3493..efd64b6f6e 100644 --- a/services/web/app/views/layout/footer.jade +++ b/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 diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 091831a22d..3bc459f290 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/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") diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 25b240c08d..47902d5826 100644 --- a/services/web/config/settings.defaults.coffee +++ b/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 diff --git a/services/web/package.json b/services/web/package.json index 8d14a350dc..86c7dee403 100644 --- a/services/web/package.json +++ b/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", diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee index 31fc11b6a3..747ca1ad08 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee +++ b/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? diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 610915ec3a..715dbb9161 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/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) -> diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee index 5639d670f0..e634afea3a 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee +++ b/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 diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee index 5f87ff25e9..669cfeced1 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee +++ b/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' diff --git a/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee b/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee index 5166a32ae1..2aa4efb505 100644 --- a/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee +++ b/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') \ No newline at end of file + $modalInstance.dismiss('cancel') diff --git a/services/web/public/img/sprite.png b/services/web/public/img/sprite.png new file mode 100644 index 0000000000..366d994ce8 Binary files /dev/null and b/services/web/public/img/sprite.png differ diff --git a/services/web/public/stylesheets/app/sprites.less b/services/web/public/stylesheets/app/sprites.less new file mode 100644 index 0000000000..b64e84ad68 --- /dev/null +++ b/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; +} diff --git a/services/web/public/stylesheets/app/wiki.less b/services/web/public/stylesheets/app/wiki.less index 2bc962961f..5f7462d02c 100644 --- a/services/web/public/stylesheets/app/wiki.less +++ b/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; } diff --git a/services/web/public/stylesheets/components/footer.less b/services/web/public/stylesheets/components/footer.less index 7a3bc66723..e4e4f82da1 100644 --- a/services/web/public/stylesheets/components/footer.less +++ b/services/web/public/stylesheets/components/footer.less @@ -23,3 +23,8 @@ footer.site-footer { } } } + +.sprite-icon-lang { + display: inline-block; + vertical-align: middle; +} \ No newline at end of file diff --git a/services/web/public/stylesheets/style.less b/services/web/public/stylesheets/style.less index b516adff3e..0377e4e828 100755 --- a/services/web/public/stylesheets/style.less +++ b/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"; diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 632b43e7f2..37e39aaacb 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/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 \ No newline at end of file diff --git a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee index 97b41fb33b..d7a13578e2 100644 --- a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee +++ b/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 diff --git a/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee index 82a4ab734b..849e9e8ccc 100644 --- a/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee +++ b/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 diff --git a/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee index 601f7867b0..6a99ed8150 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee +++ b/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"