Merge branch 'master' into sk-session-revocation

This commit is contained in:
Shane Kilkelly 2016-07-06 13:19:15 +01:00
commit fc6cf75ad5
27 changed files with 244 additions and 76 deletions

View file

@ -17,8 +17,9 @@ module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-contrib-watch' grunt.loadNpmTasks 'grunt-contrib-watch'
grunt.loadNpmTasks 'grunt-parallel' grunt.loadNpmTasks 'grunt-parallel'
grunt.loadNpmTasks 'grunt-exec' grunt.loadNpmTasks 'grunt-exec'
grunt.loadNpmTasks 'grunt-contrib-imagemin'
grunt.loadNpmTasks 'grunt-contrib-cssmin' grunt.loadNpmTasks 'grunt-contrib-cssmin'
# grunt.loadNpmTasks 'grunt-contrib-imagemin'
# grunt.loadNpmTasks 'grunt-sprity'
config = config =
@ -47,18 +48,26 @@ module.exports = (grunt) ->
stream:true stream:true
imagemin: # imagemin:
dynamic: # dynamic:
files: [{ # files: [{
expand: true # expand: true
cwd: 'public/img/' # cwd: 'public/img/'
src: ['**/*.{png,jpg,gif}'] # src: ['**/*.{png,jpg,gif}']
dest: 'public/img/' # dest: 'public/img/'
}] # }]
options: # options:
interlaced:false # interlaced:false
optimizationLevel: 7 # 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: coffee:
@ -220,6 +229,7 @@ module.exports = (grunt) ->
availabletasks: availabletasks:
tasks: tasks:
options: options:

View file

@ -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 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. 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 The rest of the ShareLaTeX stack, along with information about contributing can be found in the
[sharelatex/sharelatex](https://github.com/sharelatex/sharelatex) repository. [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 Unit test status
---------------- ----------------

View file

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

View file

@ -30,12 +30,17 @@ module.exports = CollaboratorsHandler =
getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
return callback(error) if error? return callback(error) if error?
result = []
async.mapLimit members, 3, async.mapLimit members, 3,
(member, cb) -> (member, cb) ->
UserGetter.getUser member.id, (error, user) -> UserGetter.getUser member.id, (error, user) ->
return cb(error) if error? return cb(error) if error?
return cb(null, { user: user, privilegeLevel: member.privilegeLevel }) if user?
callback 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) ->) -> getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
# In future if the schema changes and getting all member ids is more expensive (multiple documents) # 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" logger.error err: err, "problem removing user from project collaberators"
callback(err) 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) ->) -> addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
emails = mimelib.parseAddresses(unparsed_email) emails = mimelib.parseAddresses(unparsed_email)
email = emails[0]?.address?.toLowerCase() email = emails[0]?.address?.toLowerCase()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -263,6 +263,10 @@ module.exports = settings =
# public projects, /learn, /templates, about pages, etc. # public projects, /learn, /templates, about pages, etc.
allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false 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. # Maximum size of text documents in the real-time editing system.
max_doc_length: 2 * 1024 * 1024 # 2mb max_doc_length: 2 * 1024 * 1024 # 2mb

View file

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

View file

@ -12,7 +12,8 @@ define [
ruleDetails = _getRule entry.message ruleDetails = _getRule entry.message
if (ruleDetails?) 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.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL? entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?

View file

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

View file

@ -12,7 +12,8 @@ define [
constructor: (@url, @options) -> constructor: (@url, @options) ->
# PDFJS.disableFontFace = true # avoids repaints, uses worker more # 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.disableStream
# PDFJS.disableRange # PDFJS.disableRange
@scale = @options.scale || 1 @scale = @options.scale || 1

View file

@ -27,13 +27,21 @@ define [
$scope.document.destroy() if $scope.document? $scope.document.destroy() if $scope.document?
$scope.loadCount = if $scope.loadCount? then $scope.loadCount + 1 else 1 $scope.loadCount = if $scope.loadCount? then $scope.loadCount + 1 else 1
# TODO need a proper url manipulation library to add to query string # 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, scale: 1,
disableAutoFetch: if onDemandLoading then true else undefined
navigateFn: (ref) -> navigateFn: (ref) ->
# this function captures clicks on the annotation links # this function captures clicks on the annotation links
$scope.navigateTo = ref $scope.navigateTo = ref
$scope.$apply() $scope.$apply()
progressCallback: (progress) -> progressCallback: (progress) ->
return if onDemandLoading is true # don't show progress for on-demand page loading
$scope.$emit 'progress', progress $scope.$emit 'progress', progress
loadedCallback: () -> loadedCallback: () ->
$scope.$emit 'loaded' $scope.$emit 'loaded'

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -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;
}

View file

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

View file

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

View file

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

View file

@ -77,17 +77,19 @@ describe "CollaboratorsHandler", ->
{ id: "read-only-ref-2", privilegeLevel: "readOnly" } { id: "read-only-ref-2", privilegeLevel: "readOnly" }
{ id: "read-write-ref-1", privilegeLevel: "readAndWrite" } { id: "read-write-ref-1", privilegeLevel: "readAndWrite" }
{ id: "read-write-ref-2", privilegeLevel: "readAndWrite" } { id: "read-write-ref-2", privilegeLevel: "readAndWrite" }
{ id: "doesnt-exist", privilegeLevel: "readAndWrite" }
]) ])
@UserGetter.getUser = sinon.stub() @UserGetter.getUser = sinon.stub()
@UserGetter.getUser.withArgs("read-only-ref-1").yields(null, { _id: "read-only-ref-1" }) @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-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-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("read-write-ref-2").yields(null, { _id: "read-write-ref-2" })
@UserGetter.getUser.withArgs("doesnt-exist").yields(null, null)
@CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback @CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback
it "should return an array of members with their privilege levels", -> it "should return an array of members with their privilege levels", ->
@callback @callback
.calledWith(undefined, [ .calledWith(null, [
{ user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" } { user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" }
{ user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" } { user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" }
{ user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite" } { user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite" }
@ -274,6 +276,19 @@ describe "CollaboratorsHandler", ->
it "should not add any users to the proejct", -> it "should not add any users to the proejct", ->
@CollaboratorHandler.addUserIdToProject.called.should.equal false @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

View file

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

View file

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

View file

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