mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge remote-tracking branch 'origin/master' into bg-improve-upload-robustness
This commit is contained in:
commit
e23871118d
47 changed files with 631 additions and 270 deletions
|
@ -34,32 +34,32 @@ module.exports = ChatController =
|
|||
res.json messages
|
||||
|
||||
_injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
|
||||
userCache = {}
|
||||
getUserDetails = (user_id, callback = (error, user) ->) ->
|
||||
return callback(null, userCache[user_id]) if userCache[user_id]?
|
||||
UserInfoManager.getPersonalInfo user_id, (err, user) ->
|
||||
return callback(error) if error?
|
||||
user = UserInfoController.formatPersonalInfo user
|
||||
userCache[user_id] = user
|
||||
callback null, user
|
||||
# There will be a lot of repitition of user_ids, so first build a list
|
||||
# of unique ones to perform db look ups on, then use these to populate the
|
||||
# user fields
|
||||
user_ids = {}
|
||||
for thread_id, thread of threads
|
||||
if thread.resolved
|
||||
user_ids[thread.resolved_by_user_id] = true
|
||||
for message in thread.messages
|
||||
user_ids[message.user_id] = true
|
||||
|
||||
jobs = []
|
||||
for thread_id, thread of threads
|
||||
do (thread) ->
|
||||
if thread.resolved
|
||||
jobs.push (cb) ->
|
||||
getUserDetails thread.resolved_by_user_id, (error, user) ->
|
||||
cb(error) if error?
|
||||
thread.resolved_by_user = user
|
||||
cb()
|
||||
for message in thread.messages
|
||||
do (message) ->
|
||||
jobs.push (cb) ->
|
||||
getUserDetails message.user_id, (error, user) ->
|
||||
cb(error) if error?
|
||||
message.user = user
|
||||
cb()
|
||||
|
||||
users = {}
|
||||
for user_id, _ of user_ids
|
||||
do (user_id) ->
|
||||
jobs.push (cb) ->
|
||||
UserInfoManager.getPersonalInfo user_id, (err, user) ->
|
||||
return cb(error) if error?
|
||||
user = UserInfoController.formatPersonalInfo user
|
||||
users[user_id] = user
|
||||
cb()
|
||||
|
||||
async.series jobs, (error) ->
|
||||
return callback(error) if error?
|
||||
for thread_id, thread of threads
|
||||
if thread.resolved
|
||||
thread.resolved_by_user = users[thread.resolved_by_user_id]
|
||||
for message in thread.messages
|
||||
message.user = users[message.user_id]
|
||||
return callback null, threads
|
|
@ -1,7 +1,7 @@
|
|||
Settings = require "settings-sharelatex"
|
||||
request = require('request')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
RedisWrapper = require("../../infrastructure/RedisWrapper")
|
||||
rclient = RedisWrapper.client("clsi_cookie")
|
||||
Cookie = require('cookie')
|
||||
logger = require "logger-sharelatex"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Settings = require('settings-sharelatex')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
RedisWrapper = require("../../infrastructure/RedisWrapper")
|
||||
rclient = RedisWrapper.client("clsi_recently_compiled")
|
||||
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||
Project = require("../../models/Project").Project
|
||||
ProjectRootDocManager = require "../Project/ProjectRootDocManager"
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
RedisWrapper = require('../../infrastructure/RedisWrapper')
|
||||
rclient = RedisWrapper.client('cooldown')
|
||||
logger = require('logger-sharelatex')
|
||||
|
||||
|
||||
COOLDOWN_IN_SECONDS = 60 * 10
|
||||
|
||||
|
||||
module.exports = CooldownManager =
|
||||
|
||||
_buildKey: (projectId) ->
|
||||
"Cooldown:{#{projectId}}"
|
||||
|
||||
putProjectOnCooldown: (projectId, callback=(err)->) ->
|
||||
logger.log {projectId}, "[Cooldown] putting project on cooldown for #{COOLDOWN_IN_SECONDS} seconds"
|
||||
rclient.set(CooldownManager._buildKey(projectId), '1', 'EX', COOLDOWN_IN_SECONDS, callback)
|
||||
|
||||
isProjectOnCooldown: (projectId, callback=(err, isOnCooldown)->) ->
|
||||
rclient.get CooldownManager._buildKey(projectId), (err, result) ->
|
||||
if err?
|
||||
return callback(err)
|
||||
callback(null, result == "1")
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
CooldownManager = require('./CooldownManager')
|
||||
logger = require('logger-sharelatex')
|
||||
|
||||
|
||||
module.exports = CooldownMiddlewear =
|
||||
|
||||
freezeProject: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
if !projectId?
|
||||
return next(new Error('[Cooldown] No projectId parameter on route'))
|
||||
CooldownManager.isProjectOnCooldown projectId, (err, projectIsOnCooldown) ->
|
||||
if err?
|
||||
return next(err)
|
||||
if projectIsOnCooldown
|
||||
logger.log {projectId}, "[Cooldown] project is on cooldown, denying request"
|
||||
return res.sendStatus(429)
|
||||
next()
|
|
@ -1,6 +1,7 @@
|
|||
request = require("request").defaults(jar: false)
|
||||
logger = require "logger-sharelatex"
|
||||
settings = require "settings-sharelatex"
|
||||
Errors = require "../Errors/Errors"
|
||||
|
||||
module.exports = DocstoreManager =
|
||||
deleteDoc: (project_id, doc_id, callback = (error) ->) ->
|
||||
|
@ -10,6 +11,10 @@ module.exports = DocstoreManager =
|
|||
return callback(error) if error?
|
||||
if 200 <= res.statusCode < 300
|
||||
callback(null)
|
||||
else if res.statusCode is 404
|
||||
error = new Errors.NotFoundError("tried to delete doc not in docstore")
|
||||
logger.error err: error, project_id: project_id, doc_id: doc_id, "tried to delete doc not in docstore"
|
||||
callback(error) # maybe suppress the error when delete doc which is not present?
|
||||
else
|
||||
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
||||
logger.error err: error, project_id: project_id, doc_id: doc_id, "error deleting doc in docstore"
|
||||
|
@ -61,6 +66,10 @@ module.exports = DocstoreManager =
|
|||
if 200 <= res.statusCode < 300
|
||||
logger.log doc_id: doc_id, project_id: project_id, version: doc.version, rev: doc.rev, "got doc from docstore api"
|
||||
callback(null, doc.lines, doc.rev, doc.version, doc.ranges)
|
||||
else if res.statusCode is 404
|
||||
error = new Errors.NotFoundError("doc not found in docstore")
|
||||
logger.error err: error, project_id: project_id, doc_id: doc_id, "doc not found in docstore"
|
||||
callback(error)
|
||||
else
|
||||
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
||||
logger.error err: error, project_id: project_id, doc_id: doc_id, "error getting doc from docstore"
|
||||
|
|
|
@ -5,24 +5,10 @@ _ = require 'underscore'
|
|||
async = require 'async'
|
||||
logger = require('logger-sharelatex')
|
||||
metrics = require('metrics-sharelatex')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(settings.redis.web)
|
||||
Project = require("../../models/Project").Project
|
||||
ProjectLocator = require('../../Features/Project/ProjectLocator')
|
||||
|
||||
module.exports = DocumentUpdaterHandler =
|
||||
|
||||
queueChange : (project_id, doc_id, change, callback = ()->)->
|
||||
jsonChange = JSON.stringify change
|
||||
doc_key = keys.combineProjectIdAndDocId(project_id, doc_id)
|
||||
multi = rclient.multi()
|
||||
multi.rpush keys.pendingUpdates(doc_id:doc_id), jsonChange
|
||||
multi.sadd keys.docsWithPendingUpdates, doc_key
|
||||
multi.rpush "pending-updates-list", doc_key
|
||||
multi.exec (error) ->
|
||||
return callback(error) if error?
|
||||
callback()
|
||||
|
||||
flushProjectToMongo: (project_id, callback = (error) ->)->
|
||||
logger.log project_id:project_id, "flushing project from document updater"
|
||||
timer = new metrics.Timer("flushing.mongo.project")
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
Settings = require 'settings-sharelatex'
|
||||
redis = require("redis-sharelatex")
|
||||
rclientPub = redis.createClient(Settings.redis.web)
|
||||
rclientSub = redis.createClient(Settings.redis.web)
|
||||
RedisWrapper = require("../../infrastructure/RedisWrapper")
|
||||
rclient = RedisWrapper.client("realtime")
|
||||
|
||||
module.exports = EditorRealTimeController =
|
||||
rclientPub: rclientPub
|
||||
rclientSub: rclientSub
|
||||
|
||||
emitToRoom: (room_id, message, payload...) ->
|
||||
@rclientPub.publish "editor-events", JSON.stringify
|
||||
rclient.publish "editor-events", JSON.stringify
|
||||
room_id: room_id
|
||||
message: message
|
||||
payload: payload
|
||||
|
|
|
@ -22,6 +22,9 @@ module.exports = ErrorController =
|
|||
if error instanceof Errors.NotFoundError
|
||||
logger.warn {err: error, url: req.url}, "not found error"
|
||||
ErrorController.notFound req, res
|
||||
else if error instanceof Errors.TooManyRequestsError
|
||||
logger.warn {err: error, url: req.url}, "too many requests error"
|
||||
res.sendStatus(429)
|
||||
else
|
||||
logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear"
|
||||
ErrorController.serverError req, res
|
||||
|
|
|
@ -11,8 +11,18 @@ ServiceNotConfiguredError = (message) ->
|
|||
error.name = "ServiceNotConfiguredError"
|
||||
error.__proto__ = ServiceNotConfiguredError.prototype
|
||||
return error
|
||||
ServiceNotConfiguredError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
TooManyRequestsError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = "TooManyRequestsError"
|
||||
error.__proto__ = TooManyRequestsError.prototype
|
||||
return error
|
||||
TooManyRequestsError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
module.exports = Errors =
|
||||
NotFoundError: NotFoundError
|
||||
ServiceNotConfiguredError: ServiceNotConfiguredError
|
||||
TooManyRequestsError: TooManyRequestsError
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
Mocha = require "mocha"
|
||||
Base = require("mocha/lib/reporters/base")
|
||||
redis = require("redis-sharelatex")
|
||||
RedisWrapper = require("../../infrastructure/RedisWrapper")
|
||||
rclient = RedisWrapper.client("health_check")
|
||||
settings = require("settings-sharelatex")
|
||||
redisCheck = redis.activeHealthCheckRedis(settings.redis.web)
|
||||
logger = require "logger-sharelatex"
|
||||
domain = require "domain"
|
||||
|
||||
|
@ -31,10 +31,12 @@ module.exports = HealthCheckController =
|
|||
delete require.cache[path]
|
||||
|
||||
checkRedis: (req, res, next)->
|
||||
if redisCheck.isAlive()
|
||||
res.sendStatus 200
|
||||
else
|
||||
res.sendStatus 500
|
||||
rclient.healthCheck (error) ->
|
||||
if error?
|
||||
logger.err {err: error}, "failed redis health check"
|
||||
res.sendStatus 500
|
||||
else
|
||||
res.sendStatus 200
|
||||
|
||||
Reporter = (res) ->
|
||||
(runner) ->
|
||||
|
|
|
@ -15,6 +15,8 @@ docComparitor = require('./DocLinesComparitor')
|
|||
projectUpdateHandler = require('./ProjectUpdateHandler')
|
||||
DocstoreManager = require "../Docstore/DocstoreManager"
|
||||
ProjectGetter = require "./ProjectGetter"
|
||||
CooldownManager = require '../Cooldown/CooldownManager'
|
||||
|
||||
|
||||
module.exports = ProjectEntityHandler =
|
||||
getAllFolders: (project_id, callback) ->
|
||||
|
@ -522,6 +524,7 @@ module.exports = ProjectEntityHandler =
|
|||
ProjectEntityHandler._countElements project, (err, count)->
|
||||
if count > settings.maxEntitiesPerProject
|
||||
logger.warn project_id:project._id, "project too big, stopping insertions"
|
||||
CooldownManager.putProjectOnCooldown(project._id)
|
||||
return callback("project_has_to_many_files")
|
||||
projectLocator.findElement {project:project, element_id:folder_id, type:"folders"}, (err, folder, path)=>
|
||||
if err?
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Settings = require('settings-sharelatex')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
RedisWrapper = require("../../infrastructure/RedisWrapper")
|
||||
rclient = RedisWrapper.client("one_time_token")
|
||||
crypto = require("crypto")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ projectCreationHandler = require('../Project/ProjectCreationHandler')
|
|||
projectDeleter = require('../Project/ProjectDeleter')
|
||||
ProjectRootDocManager = require "../Project/ProjectRootDocManager"
|
||||
FileTypeManager = require('../Uploads/FileTypeManager')
|
||||
CooldownManager = require('../Cooldown/CooldownManager')
|
||||
Errors = require('../Errors/Errors')
|
||||
|
||||
commitMessage = "Before update from Dropbox"
|
||||
|
||||
|
@ -24,10 +26,15 @@ module.exports =
|
|||
cb err, project
|
||||
getOrCreateProject (err, project)->
|
||||
return callback(err) if err?
|
||||
FileTypeManager.shouldIgnore path, (err, shouldIgnore)->
|
||||
if shouldIgnore
|
||||
return callback()
|
||||
updateMerger.mergeUpdate user_id, project._id, path, updateRequest, source, callback
|
||||
CooldownManager.isProjectOnCooldown project._id, (err, projectIsOnCooldown) ->
|
||||
return callback(err) if err?
|
||||
if projectIsOnCooldown
|
||||
logger.log {projectId: project._id}, "project is on cooldown, denying request"
|
||||
return callback(new Errors.TooManyRequestsError('project on cooldown'))
|
||||
FileTypeManager.shouldIgnore path, (err, shouldIgnore)->
|
||||
if shouldIgnore
|
||||
return callback()
|
||||
updateMerger.mergeUpdate user_id, project._id, path, updateRequest, source, callback
|
||||
|
||||
|
||||
deleteUpdate: (user_id, projectName, path, source, callback)->
|
||||
|
|
|
@ -3,7 +3,6 @@ logger = require("logger-sharelatex")
|
|||
Async = require('async')
|
||||
_ = require('underscore')
|
||||
UserSessionsRedis = require('./UserSessionsRedis')
|
||||
|
||||
rclient = UserSessionsRedis.client()
|
||||
|
||||
module.exports = UserSessionsManager =
|
||||
|
|
|
@ -1,21 +1,9 @@
|
|||
Settings = require 'settings-sharelatex'
|
||||
redis = require 'redis-sharelatex'
|
||||
ioredis = require 'ioredis'
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
redisSessionsSettings = Settings.redis.websessions or Settings.redis.web
|
||||
RedisWrapper = require("../../infrastructure/RedisWrapper")
|
||||
rclient = RedisWrapper.client("websessions")
|
||||
|
||||
module.exports = Redis =
|
||||
client: () ->
|
||||
if redisSessionsSettings?.cluster?
|
||||
logger.log {}, "using redis cluster for web sessions"
|
||||
rclient = new ioredis.Cluster(redisSessionsSettings.cluster)
|
||||
else
|
||||
rclient = redis.createClient(redisSessionsSettings)
|
||||
return rclient
|
||||
|
||||
sessionSetKey: (user) ->
|
||||
if redisSessionsSettings?.cluster?
|
||||
return "UserSessions:{#{user._id}}"
|
||||
else
|
||||
return "UserSessions:#{user._id}"
|
||||
return "UserSessions:{#{user._id}}"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
metrics = require('metrics-sharelatex')
|
||||
Settings = require('settings-sharelatex')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
RedisWrapper = require("./RedisWrapper")
|
||||
rclient = RedisWrapper.client("lock")
|
||||
logger = require "logger-sharelatex"
|
||||
|
||||
module.exports = LockManager =
|
||||
|
@ -9,17 +9,17 @@ module.exports = LockManager =
|
|||
MAX_LOCK_WAIT_TIME: 10000 # 10s maximum time to spend trying to get the lock
|
||||
REDIS_LOCK_EXPIRY: 30 # seconds. Time until lock auto expires in redis.
|
||||
|
||||
_blockingKey : (key)-> "Blocking:"+key
|
||||
_blockingKey : (key)-> "lock:web:{#{key}}"
|
||||
|
||||
tryLock : (key, callback = (err, isFree)->)->
|
||||
rclient.set LockManager._blockingKey(key), "locked", "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)->
|
||||
return callback(err) if err?
|
||||
if gotLock == "OK"
|
||||
metrics.inc "doc-not-blocking"
|
||||
metrics.inc "lock-not-blocking"
|
||||
callback err, true
|
||||
else
|
||||
metrics.inc "doc-blocking"
|
||||
logger.log key: key, redis_response: gotLock, "doc is locked"
|
||||
metrics.inc "lock-blocking"
|
||||
logger.log key: key, redis_response: gotLock, "lock is locked"
|
||||
callback err, false
|
||||
|
||||
getLock: (key, callback = (error) ->) ->
|
||||
|
@ -42,10 +42,10 @@ module.exports = LockManager =
|
|||
return callback(err) if err?
|
||||
exists = parseInt replys[0]
|
||||
if exists == 1
|
||||
metrics.inc "doc-blocking"
|
||||
metrics.inc "lock-blocking"
|
||||
callback err, false
|
||||
else
|
||||
metrics.inc "doc-not-blocking"
|
||||
metrics.inc "lock-not-blocking"
|
||||
callback err, true
|
||||
|
||||
releaseLock: (key, callback)->
|
||||
|
|
|
@ -1,28 +1,14 @@
|
|||
Settings = require 'settings-sharelatex'
|
||||
redis = require 'redis-sharelatex'
|
||||
ioredis = require 'ioredis'
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
|
||||
# A per-feature interface to Redis,
|
||||
# looks up the feature in `settings.redis`
|
||||
# and returns an appropriate client.
|
||||
# Necessary because we don't want to migrate web over
|
||||
# to redis-cluster all at once.
|
||||
|
||||
# TODO: consider merging into `redis-sharelatex`
|
||||
|
||||
|
||||
module.exports = Redis =
|
||||
|
||||
# feature = 'websessions' | 'ratelimiter' | ...
|
||||
client: (feature) ->
|
||||
redisFeatureSettings = Settings.redis[feature] or Settings.redis.web
|
||||
if redisFeatureSettings?.cluster?
|
||||
logger.log {feature}, "creating redis-cluster client"
|
||||
rclient = new ioredis.Cluster(redisFeatureSettings.cluster)
|
||||
rclient.__is_redis_cluster = true
|
||||
else
|
||||
logger.log {feature}, "creating redis client"
|
||||
rclient = redis.createClient(redisFeatureSettings)
|
||||
rclient = redis.createClient(redisFeatureSettings)
|
||||
return rclient
|
||||
|
|
|
@ -32,6 +32,7 @@ ChatController = require("./Features/Chat/ChatController")
|
|||
BlogController = require("./Features/Blog/BlogController")
|
||||
Modules = require "./infrastructure/Modules"
|
||||
RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear')
|
||||
CooldownMiddlewear = require('./Features/Cooldown/CooldownMiddlewear')
|
||||
RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter')
|
||||
InactiveProjectController = require("./Features/InactiveData/InactiveProjectController")
|
||||
ContactRouter = require("./Features/Contacts/ContactRouter")
|
||||
|
|
|
@ -16,7 +16,7 @@ div.full-size(
|
|||
'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\
|
||||
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
|
||||
'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
|
||||
'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\
|
||||
'rp-size-mini': ui.miniReviewPanelVisible,\
|
||||
'rp-size-expanded': ui.reviewPanelOpen,\
|
||||
'rp-layout-left': reviewPanel.layoutToLeft,\
|
||||
'rp-loading-threads': reviewPanel.loadingThreads,\
|
||||
|
|
|
@ -15,7 +15,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
|
|||
i.fa.fa-fw.fa-level-up
|
||||
|
||||
span(ng-controller="PdfViewToggleController")
|
||||
a(
|
||||
a.btn.btn-full-height.btn-full-height-no-border(
|
||||
href,
|
||||
ng-show="ui.pdfLayout == 'flat' && fileTreeClosed",
|
||||
tooltip="PDF",
|
||||
|
|
|
@ -140,23 +140,35 @@
|
|||
ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
|
||||
ng-controller="ProjectListItemController"
|
||||
)
|
||||
.row
|
||||
.row(select-row)
|
||||
.col-xs-6
|
||||
input.select-item(
|
||||
select-individual,
|
||||
type="checkbox",
|
||||
ng-model="project.selected"
|
||||
stop-propagation="click"
|
||||
)
|
||||
span
|
||||
a.projectName(href="/project/{{project.id}}") {{project.name}}
|
||||
a.projectName(
|
||||
href="/project/{{project.id}}"
|
||||
stop-propagation="click"
|
||||
) {{project.name}}
|
||||
span(
|
||||
ng-controller="TagListController"
|
||||
)
|
||||
a.label.label-default.tag-label(
|
||||
href,
|
||||
ng-repeat='tag in project.tags',
|
||||
ng-click="selectTag(tag)"
|
||||
) {{tag.name}}
|
||||
.tag-label(
|
||||
ng-repeat='tag in project.tags'
|
||||
stop-propagation="click"
|
||||
)
|
||||
a.label.label-default.tag-label-name(
|
||||
href,
|
||||
ng-click="selectTag(tag)"
|
||||
) {{tag.name}}
|
||||
a.label.label-default.tag-label-remove(
|
||||
href
|
||||
ng-click="removeProjectFromTag(project, tag)"
|
||||
) ×
|
||||
|
||||
.col-xs-2
|
||||
span.owner {{ownerName()}}
|
||||
.col-xs-4
|
||||
|
|
|
@ -79,6 +79,15 @@
|
|||
li
|
||||
a(href, ng-click="deleteTag(tag)", stop-propagation="click")
|
||||
| #{translate("delete")}
|
||||
li.tag.untagged(
|
||||
ng-if="tags.length",
|
||||
ng-cloak,
|
||||
ng-click="selectUntagged()"
|
||||
ng-class="{active: filter === 'untagged'}",
|
||||
)
|
||||
a.tag-name(href)
|
||||
| #{translate("uncategorized")}
|
||||
span.subdued ({{ nUntagged }})
|
||||
li(ng-cloak)
|
||||
a.tag(href, ng-click="openNewTagModal()")
|
||||
i.fa.fa-fw.fa-plus
|
||||
|
|
|
@ -58,6 +58,16 @@ module.exports = settings =
|
|||
# {host: 'localhost', port: 7005}
|
||||
# ]
|
||||
|
||||
# cooldown:
|
||||
# cluster: [
|
||||
# {host: 'localhost', port: 7000}
|
||||
# {host: 'localhost', port: 7001}
|
||||
# {host: 'localhost', port: 7002}
|
||||
# {host: 'localhost', port: 7003}
|
||||
# {host: 'localhost', port: 7004}
|
||||
# {host: 'localhost', port: 7005}
|
||||
# ]
|
||||
|
||||
api:
|
||||
host: "localhost"
|
||||
port: "6379"
|
||||
|
|
59
services/web/npm-shrinkwrap.json
generated
59
services/web/npm-shrinkwrap.json
generated
|
@ -740,12 +740,6 @@
|
|||
"from": "double-ended-queue@>=2.1.0-0 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz"
|
||||
},
|
||||
"dtrace-provider": {
|
||||
"version": "0.2.8",
|
||||
"from": "dtrace-provider@0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz",
|
||||
"optional": true
|
||||
},
|
||||
"each-series": {
|
||||
"version": "1.0.0",
|
||||
"from": "each-series@>=1.0.0 <2.0.0",
|
||||
|
@ -2742,18 +2736,18 @@
|
|||
}
|
||||
},
|
||||
"redis-sharelatex": {
|
||||
"version": "0.0.9",
|
||||
"from": "redis-sharelatex@0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/redis-sharelatex/-/redis-sharelatex-0.0.9.tgz",
|
||||
"version": "1.0.2",
|
||||
"from": "git+https://github.com/sharelatex/redis-sharelatex.git#v1.0.2",
|
||||
"resolved": "git+https://github.com/sharelatex/redis-sharelatex.git#143b7eb192675f36d835080e534a4ac4899f918a",
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "0.2.1",
|
||||
"from": "ansi-regex@^0.2.0",
|
||||
"from": "ansi-regex@>=0.2.0 <0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz"
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "1.1.0",
|
||||
"from": "ansi-styles@^1.1.0",
|
||||
"from": "ansi-styles@>=1.1.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz"
|
||||
},
|
||||
"assertion-error": {
|
||||
|
@ -2761,6 +2755,11 @@
|
|||
"from": "assertion-error@1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz"
|
||||
},
|
||||
"async": {
|
||||
"version": "2.4.0",
|
||||
"from": "async@>=2.4.0 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.4.0.tgz"
|
||||
},
|
||||
"chai": {
|
||||
"version": "1.9.1",
|
||||
"from": "chai@1.9.1",
|
||||
|
@ -2768,7 +2767,7 @@
|
|||
},
|
||||
"chalk": {
|
||||
"version": "0.5.1",
|
||||
"from": "chalk@~0.5.0",
|
||||
"from": "chalk@>=0.5.0 <0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz"
|
||||
},
|
||||
"coffee-script": {
|
||||
|
@ -2793,7 +2792,7 @@
|
|||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"from": "mkdirp@^0.5.0",
|
||||
"from": "mkdirp@>=0.5.0 <0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz"
|
||||
}
|
||||
}
|
||||
|
@ -2815,8 +2814,13 @@
|
|||
"dependencies": {
|
||||
"coffee-script": {
|
||||
"version": "1.7.1",
|
||||
"from": "coffee-script@~1.7.0",
|
||||
"from": "coffee-script@>=1.7.0 <1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz"
|
||||
},
|
||||
"lodash": {
|
||||
"version": "2.4.2",
|
||||
"from": "lodash@>=2.4.1 <2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -2827,7 +2831,7 @@
|
|||
},
|
||||
"has-ansi": {
|
||||
"version": "0.1.0",
|
||||
"from": "has-ansi@^0.1.0",
|
||||
"from": "has-ansi@>=0.1.0 <0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz"
|
||||
},
|
||||
"jade": {
|
||||
|
@ -2852,15 +2856,10 @@
|
|||
"from": "jsonfile@>=2.0.0 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz"
|
||||
},
|
||||
"lodash": {
|
||||
"version": "2.4.2",
|
||||
"from": "lodash@~2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.4",
|
||||
"from": "minimatch@>=3.0.2 <4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz"
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz"
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.3.5",
|
||||
|
@ -2884,7 +2883,7 @@
|
|||
},
|
||||
"minimatch": {
|
||||
"version": "0.2.14",
|
||||
"from": "minimatch@~0.2.11",
|
||||
"from": "minimatch@>=0.2.11 <0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz"
|
||||
}
|
||||
}
|
||||
|
@ -2904,11 +2903,6 @@
|
|||
"from": "rimraf@>=2.2.8 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz"
|
||||
},
|
||||
"samsam": {
|
||||
"version": "1.1.3",
|
||||
"from": "samsam@>=1.1.0 <1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.3.tgz"
|
||||
},
|
||||
"sandboxed-module": {
|
||||
"version": "1.0.1",
|
||||
"from": "sandboxed-module@1.0.1",
|
||||
|
@ -2921,12 +2915,12 @@
|
|||
},
|
||||
"strip-ansi": {
|
||||
"version": "0.3.0",
|
||||
"from": "strip-ansi@^0.3.0",
|
||||
"from": "strip-ansi@>=0.3.0 <0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz"
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "0.2.0",
|
||||
"from": "supports-color@^0.2.0",
|
||||
"from": "supports-color@>=0.2.0 <0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz"
|
||||
},
|
||||
"underscore": {
|
||||
|
@ -3036,6 +3030,11 @@
|
|||
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz",
|
||||
"optional": true
|
||||
},
|
||||
"samsam": {
|
||||
"version": "1.1.2",
|
||||
"from": "samsam@1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz"
|
||||
},
|
||||
"sanitizer": {
|
||||
"version": "0.1.1",
|
||||
"from": "sanitizer@0.1.1",
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"passport-ldapauth": "^0.6.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"redis": "0.10.1",
|
||||
"redis-sharelatex": "0.0.9",
|
||||
"redis-sharelatex": "git+https://github.com/sharelatex/redis-sharelatex.git#v1.0.2",
|
||||
"request": "^2.69.0",
|
||||
"requests": "^0.1.7",
|
||||
"rimraf": "2.2.6",
|
||||
|
|
|
@ -58,4 +58,18 @@ define [
|
|||
scope.$apply () ->
|
||||
scope.ngModel = false
|
||||
ignoreChanges = false
|
||||
|
||||
scope.$on "select-all:row-clicked", () ->
|
||||
ignoreChanges = true
|
||||
scope.$apply () ->
|
||||
scope.ngModel = !scope.ngModel
|
||||
ignoreChanges = false
|
||||
}
|
||||
|
||||
App.directive "selectRow", () ->
|
||||
return {
|
||||
scope: true
|
||||
link: (scope, element, attrs) ->
|
||||
element.on "click", (e) ->
|
||||
scope.$broadcast "select-all:row-clicked"
|
||||
}
|
|
@ -70,6 +70,7 @@ define [
|
|||
chatOpen: false
|
||||
pdfLayout: 'sideBySide'
|
||||
reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}")
|
||||
miniReviewPanelVisible: false
|
||||
}
|
||||
$scope.user = window.user
|
||||
|
||||
|
|
|
@ -126,6 +126,12 @@ define [
|
|||
else
|
||||
$reviewPanelEl.css "right", "0"
|
||||
|
||||
$scope.$watch "!ui.reviewPanelOpen && reviewPanel.hasEntries", (open, prevVal) ->
|
||||
return if !open?
|
||||
$scope.ui.miniReviewPanelVisible = open
|
||||
if open != prevVal
|
||||
$timeout () -> $scope.$broadcast "review-panel:toggle"
|
||||
|
||||
$scope.$watch "ui.reviewPanelOpen", (open) ->
|
||||
return if !open?
|
||||
if !open
|
||||
|
@ -135,7 +141,10 @@ define [
|
|||
else
|
||||
# Reset back to what we had when previously open
|
||||
$scope.reviewPanel.subView = $scope.reviewPanel.openSubView
|
||||
|
||||
$timeout () ->
|
||||
$scope.$broadcast "review-panel:toggle"
|
||||
$scope.$broadcast "review-panel:layout", false
|
||||
|
||||
$scope.$watch "reviewPanel.subView", (view) ->
|
||||
return if !view?
|
||||
updateScrollbar()
|
||||
|
@ -165,12 +174,6 @@ define [
|
|||
), (nEntries) ->
|
||||
$scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChangesVisible
|
||||
|
||||
$scope.$watch "ui.reviewPanelOpen", (reviewPanelOpen) ->
|
||||
return if !reviewPanelOpen?
|
||||
$timeout () ->
|
||||
$scope.$broadcast "review-panel:toggle"
|
||||
$scope.$broadcast "review-panel:layout", false
|
||||
|
||||
regenerateTrackChangesId = (doc) ->
|
||||
old_id = getChangeTracker(doc.doc_id).getIdSeed()
|
||||
new_id = RangesTracker.generateIdSeed()
|
||||
|
|
|
@ -2,7 +2,7 @@ define [
|
|||
"base"
|
||||
], (App) ->
|
||||
|
||||
App.controller "ProjectPageController", ($scope, $modal, $q, $window, queuedHttp, event_tracking, $timeout) ->
|
||||
App.controller "ProjectPageController", ($scope, $modal, $q, $window, queuedHttp, event_tracking, $timeout, localStorage) ->
|
||||
$scope.projects = window.data.projects
|
||||
$scope.tags = window.data.tags
|
||||
$scope.notifications = window.data.notifications
|
||||
|
@ -10,6 +10,7 @@ define [
|
|||
$scope.selectedProjects = []
|
||||
$scope.filter = "all"
|
||||
$scope.predicate = "lastUpdated"
|
||||
$scope.nUntagged = 0
|
||||
$scope.reverse = true
|
||||
$scope.searchText =
|
||||
value : ""
|
||||
|
@ -18,6 +19,12 @@ define [
|
|||
recalculateProjectListHeight()
|
||||
, 10
|
||||
|
||||
$scope.$watch((
|
||||
() -> $scope.projects.filter((project) -> !project.tags? or project.tags.length == 0).length
|
||||
), (newVal) -> $scope.nUntagged = newVal)
|
||||
|
||||
storedUIOpts = JSON.parse(localStorage("project_list"))
|
||||
|
||||
recalculateProjectListHeight = () ->
|
||||
topOffset = $(".project-list-card")?.offset()?.top
|
||||
bottomOffset = $("footer").outerHeight() + 25
|
||||
|
@ -52,6 +59,13 @@ define [
|
|||
project.tags ||= []
|
||||
project.tags.push tag
|
||||
|
||||
markTagAsSelected = (id) ->
|
||||
for tag in $scope.tags
|
||||
if tag._id == id
|
||||
tag.selected = true
|
||||
else
|
||||
tag.selected = false
|
||||
|
||||
$scope.changePredicate = (newPredicate)->
|
||||
if $scope.predicate == newPredicate
|
||||
$scope.reverse = !$scope.reverse
|
||||
|
@ -104,6 +118,10 @@ define [
|
|||
if $scope.filter == "tag" and selectedTag? and project.id not in selectedTag.project_ids
|
||||
visible = false
|
||||
|
||||
# Hide tagged projects if we only want to see the uncategorized ones
|
||||
if $scope.filter == "untagged" and project.tags?.length > 0
|
||||
visible = false
|
||||
|
||||
# Hide projects we own if we only want to see shared projects
|
||||
if $scope.filter == "shared" and project.accessLevel == "owner"
|
||||
visible = false
|
||||
|
@ -126,6 +144,11 @@ define [
|
|||
else
|
||||
# We don't want hidden selections
|
||||
project.selected = false
|
||||
|
||||
localStorage("project_list", JSON.stringify({
|
||||
filter: $scope.filter,
|
||||
selectedTagId: selectedTag?._id
|
||||
}))
|
||||
$scope.updateSelectedProjects()
|
||||
|
||||
$scope.getSelectedTag = () ->
|
||||
|
@ -178,6 +201,23 @@ define [
|
|||
# the projects from view
|
||||
$scope.updateVisibleProjects()
|
||||
|
||||
$scope.removeProjectFromTag = (project, tag) ->
|
||||
tag.showWhenEmpty = true
|
||||
|
||||
project.tags ||= []
|
||||
index = project.tags.indexOf tag
|
||||
|
||||
if index > -1
|
||||
$scope._removeProjectIdsFromTagArray(tag, [ project.id ])
|
||||
project.tags.splice(index, 1)
|
||||
queuedHttp({
|
||||
method: "DELETE"
|
||||
url: "/tag/#{tag._id}/project/#{project.id}"
|
||||
headers:
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
})
|
||||
$scope.updateVisibleProjects()
|
||||
|
||||
$scope.addSelectedProjectsToTag = (tag) ->
|
||||
selected_projects = $scope.getSelectedProjects()
|
||||
event_tracking.send 'project-list-page-interaction', 'project action', 'addSelectedProjectsToTag'
|
||||
|
@ -425,7 +465,12 @@ define [
|
|||
|
||||
window.location = path
|
||||
|
||||
$scope.updateVisibleProjects()
|
||||
if storedUIOpts?.filter?
|
||||
if storedUIOpts.filter == "tag" and storedUIOpts.selectedTagId?
|
||||
markTagAsSelected(storedUIOpts.selectedTagId)
|
||||
$scope.setFilter(storedUIOpts.filter)
|
||||
else
|
||||
$scope.updateVisibleProjects()
|
||||
|
||||
App.controller "ProjectListItemController", ($scope) ->
|
||||
$scope.ownerName = () ->
|
||||
|
|
|
@ -15,7 +15,11 @@ define [
|
|||
$scope._clearTags()
|
||||
tag.selected = true
|
||||
$scope.setFilter("tag")
|
||||
|
||||
|
||||
$scope.selectUntagged = () ->
|
||||
$scope._clearTags()
|
||||
$scope.setFilter("untagged")
|
||||
|
||||
$scope.deleteTag = (tag) ->
|
||||
modalInstance = $modal.open(
|
||||
templateUrl: "deleteTagModalTemplate"
|
||||
|
|
|
@ -65,6 +65,10 @@
|
|||
right: 4px;
|
||||
}
|
||||
}
|
||||
.btn-full-height-no-border {
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
float: left;
|
||||
|
|
|
@ -124,6 +124,20 @@ ul.folders-menu {
|
|||
}
|
||||
}
|
||||
}
|
||||
&.untagged {
|
||||
font-style: italic;
|
||||
margin-bottom: @line-height-computed / 4;
|
||||
a {
|
||||
line-height: 1.7;
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
span.subdued {
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
&:not(.active) {
|
||||
background-color: darken(@gray-lightest, 2%);
|
||||
|
@ -246,10 +260,24 @@ ul.project-list {
|
|||
margin-left: @line-height-computed / 4;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
padding-top: 0.25em;
|
||||
display: inline-block;
|
||||
color: white;
|
||||
}
|
||||
.tag-label-name,
|
||||
.tag-label-remove {
|
||||
display: inline-block;
|
||||
padding-top: 0.3em;
|
||||
color: #FFF;
|
||||
}
|
||||
.tag-label-name {
|
||||
padding-right: 0.3em;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.tag-label-remove {
|
||||
padding-left: 0.3em;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
i.tablesort {
|
||||
padding-left: 8px;
|
||||
|
|
|
@ -34,8 +34,8 @@ describe "ClsiCookieManager", ->
|
|||
ttl:Math.random()
|
||||
key: "coooookie"
|
||||
@requires =
|
||||
"redis-sharelatex" :
|
||||
createClient: =>
|
||||
"../../infrastructure/RedisWrapper":
|
||||
client: =>
|
||||
@redis
|
||||
"settings-sharelatex": @settings
|
||||
"request": @request
|
||||
|
|
|
@ -15,8 +15,8 @@ describe "CompileManager", ->
|
|||
@CompileManager = SandboxedModule.require modulePath, requires:
|
||||
"settings-sharelatex": @settings =
|
||||
redis: web: {host: "localhost", port: 42}
|
||||
"redis-sharelatex":
|
||||
createClient: () => @rclient = { auth: () -> }
|
||||
"../../infrastructure/RedisWrapper":
|
||||
client: () => @rclient = { auth: () -> }
|
||||
"../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler = {}
|
||||
"../Project/ProjectRootDocManager": @ProjectRootDocManager = {}
|
||||
"../../models/Project": Project: @Project = {}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
SandboxedModule = require('sandboxed-module')
|
||||
sinon = require('sinon')
|
||||
require('chai').should()
|
||||
expect = require('chai').expect
|
||||
modulePath = require('path').join __dirname, '../../../../app/js/Features/Cooldown/CooldownManager'
|
||||
|
||||
|
||||
describe "CooldownManager", ->
|
||||
|
||||
beforeEach ->
|
||||
@projectId = 'abcdefg'
|
||||
@rclient = {set: sinon.stub(), get: sinon.stub()}
|
||||
@RedisWrapper =
|
||||
client: () => @rclient
|
||||
@CooldownManager = SandboxedModule.require modulePath, requires:
|
||||
'../../infrastructure/RedisWrapper': @RedisWrapper
|
||||
'logger-sharelatex': {log: sinon.stub()}
|
||||
|
||||
describe '_buildKey', ->
|
||||
|
||||
it 'should build a properly formatted redis key', ->
|
||||
expect(@CooldownManager._buildKey('ABC')).to.equal 'Cooldown:{ABC}'
|
||||
|
||||
describe 'isProjectOnCooldown', ->
|
||||
beforeEach ->
|
||||
@call = (cb) =>
|
||||
@CooldownManager.isProjectOnCooldown @projectId, cb
|
||||
|
||||
describe 'when project is on cooldown', ->
|
||||
beforeEach ->
|
||||
@rclient.get = sinon.stub().callsArgWith(1, null, '1')
|
||||
|
||||
it 'should fetch key from redis', (done) ->
|
||||
@call (err, result) =>
|
||||
@rclient.get.callCount.should.equal 1
|
||||
@rclient.get.calledWith('Cooldown:{abcdefg}').should.equal true
|
||||
done()
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.equal null
|
||||
done()
|
||||
|
||||
it 'should produce a true result', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(result).to.equal true
|
||||
done()
|
||||
|
||||
describe 'when project is not on cooldown', ->
|
||||
beforeEach ->
|
||||
@rclient.get = sinon.stub().callsArgWith(1, null, null)
|
||||
|
||||
it 'should fetch key from redis', (done) ->
|
||||
@call (err, result) =>
|
||||
@rclient.get.callCount.should.equal 1
|
||||
@rclient.get.calledWith('Cooldown:{abcdefg}').should.equal true
|
||||
done()
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.equal null
|
||||
done()
|
||||
|
||||
it 'should produce a false result', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(result).to.equal false
|
||||
done()
|
||||
|
||||
describe 'when rclient.get produces an error', ->
|
||||
beforeEach ->
|
||||
@rclient.get = sinon.stub().callsArgWith(1, new Error('woops'))
|
||||
|
||||
it 'should fetch key from redis', (done) ->
|
||||
@call (err, result) =>
|
||||
@rclient.get.callCount.should.equal 1
|
||||
@rclient.get.calledWith('Cooldown:{abcdefg}').should.equal true
|
||||
done()
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.not.equal null
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe 'putProjectOnCooldown', ->
|
||||
|
||||
beforeEach ->
|
||||
@call = (cb) =>
|
||||
@CooldownManager.putProjectOnCooldown @projectId, cb
|
||||
|
||||
describe 'when rclient.set does not produce an error', ->
|
||||
beforeEach ->
|
||||
@rclient.set = sinon.stub().callsArgWith(4, null)
|
||||
|
||||
it 'should set a key in redis', (done) ->
|
||||
@call (err) =>
|
||||
@rclient.set.callCount.should.equal 1
|
||||
@rclient.set.calledWith('Cooldown:{abcdefg}').should.equal true
|
||||
done()
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err) =>
|
||||
expect(err).to.equal null
|
||||
done()
|
||||
|
||||
describe 'when rclient.set produces an error', ->
|
||||
beforeEach ->
|
||||
@rclient.set = sinon.stub().callsArgWith(4, new Error('woops'))
|
||||
|
||||
it 'should set a key in redis', (done) ->
|
||||
@call (err) =>
|
||||
@rclient.set.callCount.should.equal 1
|
||||
@rclient.set.calledWith('Cooldown:{abcdefg}').should.equal true
|
||||
done()
|
||||
|
||||
it 'produce an error', (done) ->
|
||||
@call (err) =>
|
||||
expect(err).to.not.equal null
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
|
@ -0,0 +1,88 @@
|
|||
SandboxedModule = require('sandboxed-module')
|
||||
sinon = require('sinon')
|
||||
require('chai').should()
|
||||
expect = require('chai').expect
|
||||
modulePath = require('path').join __dirname, '../../../../app/js/Features/Cooldown/CooldownMiddlewear'
|
||||
|
||||
|
||||
describe "CooldownMiddlewear", ->
|
||||
|
||||
beforeEach ->
|
||||
@CooldownManager =
|
||||
isProjectOnCooldown: sinon.stub()
|
||||
@CooldownMiddlewear = SandboxedModule.require modulePath, requires:
|
||||
'./CooldownManager': @CooldownManager
|
||||
'logger-sharelatex': {log: sinon.stub()}
|
||||
|
||||
describe 'freezeProject', ->
|
||||
|
||||
describe 'when project is on cooldown', ->
|
||||
beforeEach ->
|
||||
@CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, true)
|
||||
@req = {params: {Project_id: 'abc'}}
|
||||
@res = {sendStatus: sinon.stub()}
|
||||
@next = sinon.stub()
|
||||
|
||||
it 'should call CooldownManager.isProjectOnCooldown', ->
|
||||
@CooldownMiddlewear.freezeProject @req, @res, @next
|
||||
@CooldownManager.isProjectOnCooldown.callCount.should.equal 1
|
||||
@CooldownManager.isProjectOnCooldown.calledWith('abc').should.equal true
|
||||
|
||||
it 'should not produce an error', ->
|
||||
@CooldownMiddlewear.freezeProject @req, @res, @next
|
||||
@next.callCount.should.equal 0
|
||||
|
||||
it 'should send a 429 status', ->
|
||||
@CooldownMiddlewear.freezeProject @req, @res, @next
|
||||
@res.sendStatus.callCount.should.equal 1
|
||||
@res.sendStatus.calledWith(429).should.equal true
|
||||
|
||||
describe 'when project is not on cooldown', ->
|
||||
beforeEach ->
|
||||
@CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, false)
|
||||
@req = {params: {Project_id: 'abc'}}
|
||||
@res = {sendStatus: sinon.stub()}
|
||||
@next = sinon.stub()
|
||||
|
||||
it 'should call CooldownManager.isProjectOnCooldown', ->
|
||||
@CooldownMiddlewear.freezeProject @req, @res, @next
|
||||
@CooldownManager.isProjectOnCooldown.callCount.should.equal 1
|
||||
@CooldownManager.isProjectOnCooldown.calledWith('abc').should.equal true
|
||||
|
||||
it 'call next with no arguments', ->
|
||||
@CooldownMiddlewear.freezeProject @req, @res, @next
|
||||
@next.callCount.should.equal 1
|
||||
expect(@next.lastCall.args.length).to.equal 0
|
||||
|
||||
describe 'when isProjectOnCooldown produces an error', ->
|
||||
beforeEach ->
|
||||
@CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, new Error('woops'))
|
||||
@req = {params: {Project_id: 'abc'}}
|
||||
@res = {sendStatus: sinon.stub()}
|
||||
@next = sinon.stub()
|
||||
|
||||
it 'should call CooldownManager.isProjectOnCooldown', ->
|
||||
@CooldownMiddlewear.freezeProject @req, @res, @next
|
||||
@CooldownManager.isProjectOnCooldown.callCount.should.equal 1
|
||||
@CooldownManager.isProjectOnCooldown.calledWith('abc').should.equal true
|
||||
|
||||
it 'call next with an error', ->
|
||||
@CooldownMiddlewear.freezeProject @req, @res, @next
|
||||
@next.callCount.should.equal 1
|
||||
expect(@next.lastCall.args[0]).to.be.instanceof Error
|
||||
|
||||
describe 'when projectId is not part of route', ->
|
||||
beforeEach ->
|
||||
@CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, true)
|
||||
@req = {params: {lol: 'abc'}}
|
||||
@res = {sendStatus: sinon.stub()}
|
||||
@next = sinon.stub()
|
||||
|
||||
it 'call next with an error', ->
|
||||
@CooldownMiddlewear.freezeProject @req, @res, @next
|
||||
@next.callCount.should.equal 1
|
||||
expect(@next.lastCall.args[0]).to.be.instanceof Error
|
||||
|
||||
it 'should not call CooldownManager.isProjectOnCooldown', ->
|
||||
@CooldownMiddlewear.freezeProject @req, @res, @next
|
||||
@CooldownManager.isProjectOnCooldown.callCount.should.equal 0
|
|
@ -3,6 +3,7 @@ chai.should()
|
|||
sinon = require("sinon")
|
||||
modulePath = "../../../../app/js/Features/Docstore/DocstoreManager"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
Errors = require "../../../../app/js/Features/Errors/Errors.js"
|
||||
|
||||
describe "DocstoreManager", ->
|
||||
beforeEach ->
|
||||
|
@ -52,6 +53,23 @@ describe "DocstoreManager", ->
|
|||
}, "error deleting doc in docstore")
|
||||
.should.equal true
|
||||
|
||||
describe "with a missing (404) response code", ->
|
||||
beforeEach ->
|
||||
@request.del = sinon.stub().callsArgWith(1, null, statusCode: 404, "")
|
||||
@DocstoreManager.deleteDoc @project_id, @doc_id, @callback
|
||||
|
||||
it "should call the callback with an error", ->
|
||||
@callback.calledWith(new Errors.NotFoundError("tried to delete doc not in docstore")).should.equal true
|
||||
|
||||
it "should log the error", ->
|
||||
@logger.error
|
||||
.calledWith({
|
||||
err: new Errors.NotFoundError("tried to delete doc not in docstore")
|
||||
project_id: @project_id
|
||||
doc_id: @doc_id
|
||||
}, "tried to delete doc not in docstore")
|
||||
.should.equal true
|
||||
|
||||
describe "updateDoc", ->
|
||||
beforeEach ->
|
||||
@lines = ["mock", "doc", "lines"]
|
||||
|
@ -153,6 +171,23 @@ describe "DocstoreManager", ->
|
|||
it "should call the callback with the lines, version and rev", ->
|
||||
@callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true
|
||||
|
||||
describe "with a missing (404) response code", ->
|
||||
beforeEach ->
|
||||
@request.get = sinon.stub().callsArgWith(1, null, statusCode: 404, "")
|
||||
@DocstoreManager.getDoc @project_id, @doc_id, @callback
|
||||
|
||||
it "should call the callback with an error", ->
|
||||
@callback.calledWith(new Errors.NotFoundError("doc not found in docstore")).should.equal true
|
||||
|
||||
it "should log the error", ->
|
||||
@logger.error
|
||||
.calledWith({
|
||||
err: new Errors.NotFoundError("doc not found in docstore")
|
||||
project_id: @project_id
|
||||
doc_id: @doc_id
|
||||
}, "doc not found in docstore")
|
||||
.should.equal true
|
||||
|
||||
describe "getAllDocs", ->
|
||||
describe "with a successful response code", ->
|
||||
beforeEach ->
|
||||
|
|
|
@ -20,10 +20,8 @@ describe 'DocumentUpdaterHandler', ->
|
|||
|
||||
@request = {}
|
||||
@projectEntityHandler = {}
|
||||
@rclient = {auth:->}
|
||||
@settings =
|
||||
apis : documentupdater: url : "http://something.com"
|
||||
redis:{web:{}}
|
||||
@handler = SandboxedModule.require modulePath, requires:
|
||||
'request': defaults:=> return @request
|
||||
'settings-sharelatex':@settings
|
||||
|
@ -31,52 +29,10 @@ describe 'DocumentUpdaterHandler', ->
|
|||
'../Project/ProjectEntityHandler':@projectEntityHandler
|
||||
"../../models/Project": Project: @Project={}
|
||||
'../../Features/Project/ProjectLocator':{}
|
||||
'redis-sharelatex' : createClient: () => @rclient
|
||||
"metrics-sharelatex":
|
||||
Timer:->
|
||||
done:->
|
||||
|
||||
describe 'queueChange', ->
|
||||
beforeEach ->
|
||||
@change = {
|
||||
"action":"removeText",
|
||||
"range":{"start":{"row":2,"column":2},"end":{"row":2,"column":3}},
|
||||
"text":"e"
|
||||
}
|
||||
@rclient.multi = sinon.stub().returns @rclient
|
||||
@rclient.exec = sinon.stub().callsArg(0)
|
||||
@rclient.rpush = sinon.stub()
|
||||
@rclient.sadd = sinon.stub()
|
||||
@callback = sinon.stub()
|
||||
|
||||
describe "successfully", ->
|
||||
beforeEach ->
|
||||
@handler.queueChange(@project_id, @doc_id, @change, @callback)
|
||||
|
||||
it "should push the change", ->
|
||||
@rclient.rpush
|
||||
.calledWith("PendingUpdates:#{@doc_id}", JSON.stringify(@change))
|
||||
.should.equal true
|
||||
|
||||
it "should notify the doc updater of the change via the pending-updates-list queue", ->
|
||||
@rclient.rpush
|
||||
.calledWith("pending-updates-list", "#{@project_id}:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
it "should push the doc id into the pending updates set", ->
|
||||
@rclient.sadd
|
||||
.calledWith("DocsWithPendingUpdates", "#{@project_id}:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
describe "with error connecting to redis during exec", ->
|
||||
beforeEach ->
|
||||
@rclient.exec = sinon.stub().callsArgWith(0, new Error("something went wrong"))
|
||||
@handler.queueChange(@project_id, @doc_id, @change, @callback)
|
||||
|
||||
it "should return an error", ->
|
||||
@callback.calledWithExactly(sinon.match(Error)).should.equal true
|
||||
|
||||
|
||||
describe 'flushProjectToMongo', ->
|
||||
beforeEach ->
|
||||
@callback = sinon.stub()
|
||||
|
|
|
@ -5,16 +5,13 @@ modulePath = require('path').join __dirname, '../../../../app/js/Features/Editor
|
|||
|
||||
describe "EditorRealTimeController", ->
|
||||
beforeEach ->
|
||||
@rclient =
|
||||
publish: sinon.stub()
|
||||
@EditorRealTimeController = SandboxedModule.require modulePath, requires:
|
||||
"redis-sharelatex":
|
||||
createClient: () ->
|
||||
auth:->
|
||||
"../../infrastructure/RedisWrapper":
|
||||
client: () => @rclient
|
||||
"../../infrastructure/Server" : io: @io = {}
|
||||
"settings-sharelatex":{redis:{}}
|
||||
@EditorRealTimeController.rclientPub = publish: sinon.stub()
|
||||
@EditorRealTimeController.rclientSub =
|
||||
subscribe: sinon.stub()
|
||||
on: sinon.stub()
|
||||
|
||||
@room_id = "room-id"
|
||||
@message = "message-to-editor"
|
||||
|
@ -25,7 +22,7 @@ describe "EditorRealTimeController", ->
|
|||
@EditorRealTimeController.emitToRoom(@room_id, @message, @payload...)
|
||||
|
||||
it "should publish the message to redis", ->
|
||||
@EditorRealTimeController.rclientPub.publish
|
||||
@rclient.publish
|
||||
.calledWith("editor-events", JSON.stringify(
|
||||
room_id: @room_id,
|
||||
message: @message
|
||||
|
|
|
@ -23,8 +23,8 @@ describe "OneTimeTokenHandler", ->
|
|||
exec:sinon.stub()
|
||||
self = @
|
||||
@OneTimeTokenHandler = SandboxedModule.require modulePath, requires:
|
||||
"redis-sharelatex" :
|
||||
createClient: =>
|
||||
"../../infrastructure/RedisWrapper" :
|
||||
client: =>
|
||||
auth:->
|
||||
multi: -> return self.redisMulti
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
SandboxedModule = require('sandboxed-module')
|
||||
sinon = require('sinon')
|
||||
require('chai').should()
|
||||
expect = require('chai').expect
|
||||
modulePath = require('path').join __dirname, '../../../../app/js/Features/ThirdPartyDataStore/TpdsUpdateHandler.js'
|
||||
|
||||
describe 'TpdsUpdateHandler', ->
|
||||
|
@ -19,6 +20,8 @@ describe 'TpdsUpdateHandler', ->
|
|||
@rootDocManager = setRootDocAutomatically:sinon.stub()
|
||||
@FileTypeManager =
|
||||
shouldIgnore: sinon.stub().callsArgWith(1, null, false)
|
||||
@CooldownManager =
|
||||
isProjectOnCooldown: sinon.stub().callsArgWith(1, null, false)
|
||||
@handler = SandboxedModule.require modulePath, requires:
|
||||
'./UpdateMerger': @updateMerger
|
||||
'./Editor/EditorController': @editorController
|
||||
|
@ -27,6 +30,7 @@ describe 'TpdsUpdateHandler', ->
|
|||
'../Project/ProjectDeleter': @projectDeleter
|
||||
"../Project/ProjectRootDocManager" : @rootDocManager
|
||||
'../Uploads/FileTypeManager': @FileTypeManager
|
||||
'../Cooldown/CooldownManager': @CooldownManager
|
||||
'logger-sharelatex': log:->
|
||||
@user_id = "dsad29jlkjas"
|
||||
@source = "dropbox"
|
||||
|
@ -67,6 +71,38 @@ describe 'TpdsUpdateHandler', ->
|
|||
@updateMerger.mergeUpdate.called.should.equal false
|
||||
done()
|
||||
|
||||
it 'should check if the project is on cooldown', (done) ->
|
||||
@CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, false)
|
||||
@projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2)
|
||||
path = "/path/here"
|
||||
update = {}
|
||||
@updateMerger.mergeUpdate = sinon.stub()
|
||||
@updateMerger.mergeUpdate.withArgs(@user_id, @project_id, path, update, @source).callsArg(5)
|
||||
@handler.newUpdate @user_id, @project.name, path, update, @source, (err) =>
|
||||
expect(err).to.be.oneOf [null, undefined]
|
||||
@CooldownManager.isProjectOnCooldown.callCount.should.equal 1
|
||||
@CooldownManager.isProjectOnCooldown.calledWith(@project_id).should.equal true
|
||||
@FileTypeManager.shouldIgnore.callCount.should.equal 1
|
||||
@updateMerger.mergeUpdate.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'should return error and not proceed with update if project is on cooldown', (done) ->
|
||||
@CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, true)
|
||||
@projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2)
|
||||
@FileTypeManager.shouldIgnore = sinon.stub().callsArgWith(1, null, false)
|
||||
path = "/path/here"
|
||||
update = {}
|
||||
@updateMerger.mergeUpdate = sinon.stub()
|
||||
@updateMerger.mergeUpdate.withArgs(@user_id, @project_id, path, update, @source).callsArg(5)
|
||||
@handler.newUpdate @user_id, @project.name, path, update, @source, (err) =>
|
||||
expect(err).to.not.be.oneOf [null, undefined]
|
||||
expect(err).to.be.instanceof Error
|
||||
@CooldownManager.isProjectOnCooldown.callCount.should.equal 1
|
||||
@CooldownManager.isProjectOnCooldown.calledWith(@project_id).should.equal true
|
||||
@FileTypeManager.shouldIgnore.callCount.should.equal 0
|
||||
@updateMerger.mergeUpdate.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
describe 'getting a delete :', ->
|
||||
it 'should call deleteEntity in the collaberation manager', (done)->
|
||||
path = "/delete/this"
|
||||
|
|
|
@ -4,10 +4,10 @@ path = require('path')
|
|||
modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js'
|
||||
project_id = 1234
|
||||
doc_id = 5678
|
||||
blockingKey = "Blocking:#{doc_id}"
|
||||
blockingKey = "lock:web:{#{doc_id}}"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe 'Lock Manager - checking the lock', ()->
|
||||
describe 'LockManager - checking the lock', ()->
|
||||
|
||||
existsStub = sinon.stub()
|
||||
setStub = sinon.stub()
|
||||
|
@ -17,8 +17,8 @@ describe 'Lock Manager - checking the lock', ()->
|
|||
mocks =
|
||||
"logger-sharelatex": log:->
|
||||
|
||||
"redis-sharelatex":
|
||||
createClient : ()->
|
||||
"./RedisWrapper":
|
||||
client: ()->
|
||||
auth:->
|
||||
multi: ->
|
||||
exists: existsStub
|
||||
|
|
|
@ -12,8 +12,8 @@ describe 'LockManager - releasing the lock', ()->
|
|||
mocks =
|
||||
"logger-sharelatex": log:->
|
||||
|
||||
"redis-sharelatex":
|
||||
createClient : ()->
|
||||
"./RedisWrapper":
|
||||
client: ()->
|
||||
auth:->
|
||||
del:deleteStub
|
||||
|
||||
|
@ -21,6 +21,6 @@ describe 'LockManager - releasing the lock', ()->
|
|||
|
||||
it 'should put a all data into memory', (done)->
|
||||
LockManager.releaseLock doc_id, ->
|
||||
deleteStub.calledWith("Blocking:#{doc_id}").should.equal true
|
||||
deleteStub.calledWith("lock:web:{#{doc_id}}").should.equal true
|
||||
done()
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ describe 'LockManager - getting the lock', ->
|
|||
beforeEach ->
|
||||
@LockManager = SandboxedModule.require modulePath, requires:
|
||||
"logger-sharelatex": log:->
|
||||
"redis-sharelatex":
|
||||
createClient : () =>
|
||||
"./RedisWrapper":
|
||||
client: ()->
|
||||
auth:->
|
||||
"settings-sharelatex":{redis:{}}
|
||||
"metrics-sharelatex": inc:->
|
||||
|
|
|
@ -9,8 +9,8 @@ describe 'LockManager - trying the lock', ->
|
|||
beforeEach ->
|
||||
@LockManager = SandboxedModule.require modulePath, requires:
|
||||
"logger-sharelatex": log:->
|
||||
"redis-sharelatex":
|
||||
createClient : () =>
|
||||
"./RedisWrapper":
|
||||
client: () =>
|
||||
auth:->
|
||||
set: @set = sinon.stub()
|
||||
"settings-sharelatex":{redis:{}}
|
||||
|
@ -24,7 +24,7 @@ describe 'LockManager - trying the lock', ->
|
|||
@LockManager.tryLock @doc_id, @callback
|
||||
|
||||
it "should set the lock key with an expiry if it is not set", ->
|
||||
@set.calledWith("Blocking:#{@doc_id}", "locked", "EX", 30, "NX")
|
||||
@set.calledWith("lock:web:{#{@doc_id}}", "locked", "EX", 30, "NX")
|
||||
.should.equal true
|
||||
|
||||
it "should return the callback with true", ->
|
||||
|
|
|
@ -9,58 +9,28 @@ SandboxedModule = require('sandboxed-module')
|
|||
describe 'RedisWrapper', ->
|
||||
|
||||
beforeEach ->
|
||||
@featureName = 'somefeature'
|
||||
@settings =
|
||||
redis:
|
||||
web:
|
||||
port:"1234"
|
||||
host:"somewhere"
|
||||
password: "password"
|
||||
somefeature: {}
|
||||
@normalRedisInstance =
|
||||
thisIsANormalRedisInstance: true
|
||||
n: 1
|
||||
@clusterRedisInstance =
|
||||
thisIsAClusterRedisInstance: true
|
||||
n: 2
|
||||
@settings = { redis: {} }
|
||||
@redis =
|
||||
createClient: sinon.stub().returns(@normalRedisInstance)
|
||||
@ioredis =
|
||||
Cluster: sinon.stub().returns(@clusterRedisInstance)
|
||||
@logger = {log: sinon.stub()}
|
||||
|
||||
createClient: sinon.stub()
|
||||
@RedisWrapper = SandboxedModule.require modulePath, requires:
|
||||
'logger-sharelatex': @logger
|
||||
'settings-sharelatex': @settings
|
||||
'redis-sharelatex': @redis
|
||||
'ioredis': @ioredis
|
||||
|
||||
describe 'client', ->
|
||||
it "should use the feature settings if present", ->
|
||||
@settings.redis =
|
||||
my_feature:
|
||||
port:"23456"
|
||||
host:"otherhost"
|
||||
password: "banana"
|
||||
@RedisWrapper.client("my_feature")
|
||||
@redis.createClient.calledWith(@settings.redis.my_feature).should.equal true
|
||||
|
||||
beforeEach ->
|
||||
@call = () =>
|
||||
@RedisWrapper.client(@featureName)
|
||||
|
||||
describe 'when feature uses cluster', ->
|
||||
|
||||
beforeEach ->
|
||||
@settings.redis.somefeature =
|
||||
cluster: [1, 2, 3]
|
||||
|
||||
it 'should return a cluster client', ->
|
||||
client = @call()
|
||||
expect(client).to.equal @clusterRedisInstance
|
||||
expect(client.__is_redis_cluster).to.equal true
|
||||
|
||||
describe 'when feature uses normal redis', ->
|
||||
|
||||
beforeEach ->
|
||||
@settings.redis.somefeature =
|
||||
port:"1234"
|
||||
host:"somewhere"
|
||||
password: "password"
|
||||
|
||||
it 'should return a regular redis client', ->
|
||||
client = @call()
|
||||
expect(client).to.equal @normalRedisInstance
|
||||
expect(client.__is_redis_cluster).to.equal undefined
|
||||
it "should use the web settings if feature not present", ->
|
||||
@settings.redis =
|
||||
web:
|
||||
port:"43"
|
||||
host:"otherhost"
|
||||
password: "banana"
|
||||
@RedisWrapper.client("my_feature")
|
||||
@redis.createClient.calledWith(@settings.redis.web).should.equal true
|
||||
|
|
Loading…
Reference in a new issue