Merge branch 'master' into sk-fix-references-full-index

This commit is contained in:
Shane Kilkelly 2018-05-14 13:45:12 +01:00
commit 06c0b45ef7
25 changed files with 323 additions and 79 deletions

View file

@ -43,7 +43,6 @@ Unit tests can be run in the `test_unit` container defined in `docker-compose.te
The makefile contains a short cut to run these:
```
make install # Only needs running once, or when npm packages are updated
make unit_test
```

View file

@ -47,6 +47,13 @@ UnsupportedExportRecordsError = (message) ->
return error
UnsupportedExportRecordsError.prototype.__proto___ = Error.prototype
V1HistoryNotSyncedError = (message) ->
error = new Error(message)
error.name = "V1HistoryNotSyncedError"
error.__proto__ = V1HistoryNotSyncedError.prototype
return error
V1HistoryNotSyncedError.prototype.__proto___ = Error.prototype
ProjectHistoryDisabledError = (message) ->
error = new Error(message)
error.name = "ProjectHistoryDisabledError "
@ -62,4 +69,5 @@ module.exports = Errors =
UnsupportedFileTypeError: UnsupportedFileTypeError
UnsupportedBrandError: UnsupportedBrandError
UnsupportedExportRecordsError: UnsupportedExportRecordsError
V1HistoryNotSyncedError: V1HistoryNotSyncedError
ProjectHistoryDisabledError: ProjectHistoryDisabledError

View file

@ -299,6 +299,8 @@ module.exports = ProjectController =
autoPairDelimiters: user.ace.autoPairDelimiters
pdfViewer : user.ace.pdfViewer
syntaxValidation: user.ace.syntaxValidation
fontFamily: user.ace.fontFamily
lineHeight: user.ace.lineHeight
}
trackChangesState: project.track_changes
privilegeLevel: privilegeLevel
@ -311,6 +313,7 @@ module.exports = ProjectController =
maxDocLength: Settings.max_doc_length
useV2History: !!project.overleaf?.history?.display
showRichText: req.query?.rt == 'true'
showPublishModal: req.query?.pm == 'true'
timer.done()
_buildProjectList: (allProjects, v1Projects = [])->

View file

@ -16,38 +16,42 @@ AnalyticsManger = require("../Analytics/AnalyticsManager")
module.exports = ProjectCreationHandler =
createBlankProject : (owner_id, projectName, projectHistoryId, callback = (error, project) ->)->
createBlankProject : (owner_id, projectName, attributes, callback = (error, project) ->)->
metrics.inc("project-creation")
if arguments.length == 3
callback = projectHistoryId
projectHistoryId = null
callback = attributes
attributes = null
ProjectDetailsHandler.validateProjectName projectName, (error) ->
return callback(error) if error?
logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
if projectHistoryId?
ProjectCreationHandler._createBlankProject owner_id, projectName, projectHistoryId, (error, project) ->
if attributes?
ProjectCreationHandler._createBlankProject owner_id, projectName, attributes, (error, project) ->
return callback(error) if error?
AnalyticsManger.recordEvent(
owner_id, 'project-imported', { projectId: project._id, projectHistoryId: projectHistoryId }
owner_id, 'project-imported', { projectId: project._id, attributes: attributes }
)
callback(error, project)
else
HistoryManager.initializeProject (error, history) ->
return callback(error) if error?
ProjectCreationHandler._createBlankProject owner_id, projectName, history?.overleaf_id, (error, project) ->
attributes = overleaf: history: id: history?.overleaf_id
ProjectCreationHandler._createBlankProject owner_id, projectName, attributes, (error, project) ->
return callback(error) if error?
AnalyticsManger.recordEvent(
owner_id, 'project-created', { projectId: project._id }
)
callback(error, project)
_createBlankProject : (owner_id, projectName, projectHistoryId, callback = (error, project) ->)->
_createBlankProject : (owner_id, projectName, attributes, callback = (error, project) ->)->
rootFolder = new Folder {'name':'rootFolder'}
project = new Project
owner_ref : new ObjectId(owner_id)
name : projectName
project.overleaf.history.id = projectHistoryId
attributes.owner_ref = new ObjectId(owner_id)
attributes.name = projectName
project = new Project attributes
Object.assign(project, attributes)
if Settings.apis?.project_history?.displayHistoryForNewProjects
project.overleaf.history.display = true
if Settings.currentImageName?

View file

@ -405,14 +405,41 @@ module.exports = ProjectEntityUpdateHandler = self =
DocumentUpdaterHandler.resyncProjectHistory project_id, projectHistoryId, docs, files, callback
_cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) ->
self._updateProjectStructureWithDeletedEntity project, entity, entityType, path, userId, (error) ->
return callback(error) if error?
if(entityType.indexOf("file") != -1)
self._cleanUpFile project, entity, path, userId, callback
else if (entityType.indexOf("doc") != -1)
self._cleanUpDoc project, entity, path, userId, callback
else if (entityType.indexOf("folder") != -1)
self._cleanUpFolder project, entity, path, userId, callback
else
callback()
# Note: the _cleanUpEntity code and _updateProjectStructureWithDeletedEntity
# methods both need to recursively iterate over the entities in folder.
# These are currently using separate implementations of the recursion. In
# future, these could be simplified using a common project entity iterator.
_updateProjectStructureWithDeletedEntity: (project, entity, entityType, entityPath, userId, callback = (error) ->) ->
# compute the changes to the project structure
if(entityType.indexOf("file") != -1)
self._cleanUpFile project, entity, path, userId, callback
changes = oldFiles: [ {file: entity, path: entityPath} ]
else if (entityType.indexOf("doc") != -1)
self._cleanUpDoc project, entity, path, userId, callback
changes = oldDocs: [ {doc: entity, path: entityPath} ]
else if (entityType.indexOf("folder") != -1)
self._cleanUpFolder project, entity, path, userId, callback
else
callback()
changes = {oldDocs: [], oldFiles: []}
_recurseFolder = (folder, folderPath) ->
for doc in folder.docs
changes.oldDocs.push {doc, path: path.join(folderPath, doc.name)}
for file in folder.fileRefs
changes.oldFiles.push {file, path: path.join(folderPath, file.name)}
for childFolder in folder.folders
_recurseFolder(childFolder, path.join(folderPath, childFolder.name))
_recurseFolder entity, entityPath
# now send the project structure changes to the docupdater
project_id = project._id.toString()
projectHistoryId = project.overleaf?.history?.id
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
_cleanUpDoc: (project, doc, path, userId, callback = (error) ->) ->
project_id = project._id.toString()
@ -429,21 +456,10 @@ module.exports = ProjectEntityUpdateHandler = self =
return callback(error) if error?
DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) ->
return callback(error) if error?
DocstoreManager.deleteDoc project_id, doc_id, (error) ->
return callback(error) if error?
changes = oldDocs: [ {doc, path} ]
projectHistoryId = project.overleaf?.history?.id
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
DocstoreManager.deleteDoc project_id, doc_id, callback
_cleanUpFile: (project, file, path, userId, callback = (error) ->) ->
ProjectEntityMongoUpdateHandler._insertDeletedFileReference project._id, file, (error) ->
return callback(error) if error?
project_id = project._id.toString()
projectHistoryId = project.overleaf?.history?.id
changes = oldFiles: [ {file, path} ]
# we are now keeping a copy of every file versio so we no longer delete
# the file from the filestore
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
ProjectEntityMongoUpdateHandler._insertDeletedFileReference project._id, file, callback
_cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) ->
jobs = []

View file

@ -81,6 +81,11 @@ module.exports = UserController =
user.ace.pdfViewer = req.body.pdfViewer
if req.body.syntaxValidation?
user.ace.syntaxValidation = req.body.syntaxValidation
if req.body.fontFamily?
user.ace.fontFamily = req.body.fontFamily
if req.body.lineHeight?
user.ace.lineHeight = req.body.lineHeight
user.save (err)->
newEmail = req.body.email?.trim().toLowerCase()
if !newEmail? or newEmail == user.email or req.externalAuthenticationSystemUsed()

View file

@ -183,6 +183,9 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
# Don't include the query string parameters, otherwise Google
# treats ?nocdn=true as the canonical version
res.locals.currentUrl = Url.parse(req.originalUrl).pathname
res.locals.capitalize = (string) ->
return "" if string.length == 0
return string.charAt(0).toUpperCase() + string.slice(1)
next()
webRouter.use (req, res, next)->
@ -321,5 +324,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
chatMessageBorderLightness : if isOl then "40%" else "70%"
chatMessageBgSaturation : if isOl then "85%" else "60%"
chatMessageBgLightness : if isOl then "40%" else "97%"
defaultFontFamily : if isOl then 'lucida' else 'monaco'
defaultLineHeight : if isOl then 'normal' else 'compact'
renderAnnouncements : !isOl
next()

View file

@ -14,9 +14,9 @@ module.exports = Features =
return Settings.enableGithubSync
when 'v1-return-message'
return Settings.accountMerge? and Settings.overleaf?
when 'publish-modal'
return Settings.showPublishModal
when 'custom-togglers'
return Settings.overleaf?
when 'templates'
return !Settings.overleaf?
else
throw new Error("unknown feature: #{feature}")

View file

@ -3,20 +3,37 @@ Settings = require('settings-sharelatex')
RedisWrapper = require("./RedisWrapper")
rclient = RedisWrapper.client("lock")
logger = require "logger-sharelatex"
os = require "os"
crypto = require "crypto"
HOST = os.hostname()
PID = process.pid
RND = crypto.randomBytes(4).toString('hex')
COUNT = 0
module.exports = LockManager =
LOCK_TEST_INTERVAL: 50 # 50ms between each test of the lock
MAX_TEST_INTERVAL: 1000 # back off to 1s between each test of the lock
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
SLOW_EXECUTION_THRESHOLD: 5000 # 5s, if execution takes longer than this then log
# Use a signed lock value as described in
# http://redis.io/topics/distlock#correct-implementation-with-a-single-instance
# to prevent accidental unlocking by multiple processes
randomLock : () ->
time = Date.now()
return "locked:host=#{HOST}:pid=#{PID}:random=#{RND}:time=#{time}:count=#{COUNT++}"
unlockScript: 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end'
runWithLock: (namespace, id, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) ->
# This error is defined here so we get a useful stacktrace
slowExecutionError = new Error "slow execution during lock"
timer = new metrics.Timer("lock.#{namespace}")
key = "lock:web:#{namespace}:#{id}"
LockManager._getLock key, namespace, (error) ->
LockManager._getLock key, namespace, (error, lockValue) ->
return callback(error) if error?
# The lock can expire in redis but the process carry on. This setTimout call
@ -27,7 +44,7 @@ module.exports = LockManager =
exceededLockTimeout = setTimeout countIfExceededLockTimeout, LockManager.REDIS_LOCK_EXPIRY * 1000
runner (error1, values...) ->
LockManager._releaseLock key, (error2) ->
LockManager._releaseLock key, lockValue, (error2) ->
clearTimeout exceededLockTimeout
timeTaken = new Date - timer.start
@ -39,19 +56,21 @@ module.exports = LockManager =
return callback(error) if error?
callback null, values...
_tryLock : (key, namespace, callback = (err, isFree)->)->
rclient.set key, "locked", "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)->
_tryLock : (key, namespace, callback = (err, isFree, lockValue)->)->
lockValue = LockManager.randomLock()
rclient.set key, lockValue, "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)->
return callback(err) if err?
if gotLock == "OK"
metrics.inc "lock.#{namespace}.try.success"
callback err, true
callback err, true, lockValue
else
metrics.inc "lock.#{namespace}.try.failed"
logger.log key: key, redis_response: gotLock, "lock is locked"
callback err, false
_getLock: (key, namespace, callback = (error) ->) ->
_getLock: (key, namespace, callback = (error, lockValue) ->) ->
startTime = Date.now()
testInterval = LockManager.LOCK_TEST_INTERVAL
attempts = 0
do attempt = () ->
if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME
@ -59,13 +78,23 @@ module.exports = LockManager =
return callback(new Error("Timeout"))
attempts += 1
LockManager._tryLock key, namespace, (error, gotLock) ->
LockManager._tryLock key, namespace, (error, gotLock, lockValue) ->
return callback(error) if error?
if gotLock
metrics.gauge "lock.#{namespace}.get.success.tries", attempts
callback(null)
callback(null, lockValue)
else
setTimeout attempt, LockManager.LOCK_TEST_INTERVAL
setTimeout attempt, testInterval
# back off when the lock is taken to avoid overloading
testInterval = Math.min(testInterval * 2, LockManager.MAX_TEST_INTERVAL)
_releaseLock: (key, callback)->
rclient.del key, callback
_releaseLock: (key, lockValue, callback)->
rclient.eval LockManager.unlockScript, 1, key, lockValue, (err, result) ->
if err?
return callback(err)
else if result? and result isnt 1 # successful unlock should release exactly one key
logger.error {key:key, lockValue:lockValue, redis_err:err, redis_result:result}, "unlocking error"
metrics.inc "unlock-error"
return callback(new Error("tried to release timed out lock"))
else
callback(null,result)

View file

@ -20,14 +20,16 @@ UserSchema = new Schema
loginCount : {type : Number, default: 0}
holdingAccount : {type : Boolean, default: false}
ace : {
mode : {type : String, default: 'none'}
theme : {type : String, default: 'textmate'}
fontSize : {type : Number, default:'12'}
autoComplete: {type : Boolean, default: true}
autoPairDelimiters: {type : Boolean, default: true}
spellCheckLanguage : {type : String, default: "en"}
pdfViewer : {type : String, default: "pdfjs"}
syntaxValidation : {type : Boolean}
mode : {type : String, default: 'none'}
theme : {type : String, default: 'textmate'}
fontSize : {type : Number, default:'12'}
autoComplete : {type : Boolean, default: true}
autoPairDelimiters : {type : Boolean, default: true}
spellCheckLanguage : {type : String, default: "en"}
pdfViewer : {type : String, default: "pdfjs"}
syntaxValidation : {type : Boolean}
fontFamily : {type : String}
lineHeight : {type : String}
}
features : {
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }

View file

@ -154,7 +154,10 @@ block requirejs
},
"ace/ext-language_tools": {
"deps": ["ace/ace"]
}
},
"ace/keybinding-vim": {
"deps": ["ace/ace"]
},
},
"config":{
"moment":{

View file

@ -58,6 +58,7 @@ div.full-size(
read-only="!permissions.write",
file-name="editor.open_doc_name",
on-ctrl-enter="recompileViaKey",
on-save="recompileViaKey",
on-ctrl-j="toggleReviewPanel",
on-ctrl-shift-c="addNewCommentFromKbdShortcut",
on-ctrl-shift-a="toggleTrackChangesFromKbdShortcut",
@ -68,6 +69,8 @@ div.full-size(
track-changes= "editor.trackChanges",
doc-id="editor.open_doc_id"
renderer-data="reviewPanel.rendererData"
font-family="settings.fontFamily || ui.defaultFontFamily"
line-height="settings.lineHeight || ui.defaultLineHeight"
)
!= moduleIncludes('editor:body', locals)

View file

@ -150,6 +150,26 @@ aside#left-menu.full-size(
each size in ['10','11','12','13','14','16','20','24']
option(value=size) #{size}px
.form-controls
label(for="fontFamily") #{translate("font_family")}
select(
name="fontFamily"
ng-model="settings.fontFamily"
)
option(value="", disabled) #{translate("default")}
each fontFamily in ['monaco', 'lucida']
option(value=fontFamily) #{capitalize(fontFamily)}
.form-controls
label(for="lineHeight") #{translate("line_height")}
select(
name="lineHeight"
ng-model="settings.lineHeight"
)
option(value="", disabled) #{translate("default")}
each lineHeight in ['compact', 'normal', 'wide']
option(value=lineHeight) #{translate(lineHeight)}
.form-controls
label(for="pdfViewer") #{translate("pdf_viewer")}
select(

View file

@ -32,14 +32,16 @@
ng-click="downloadSelectedProjects()"
)
i.fa.fa-cloud-download
- var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete")
- var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o"
a.btn.btn-default(
href,
tooltip=translate('delete'),
tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`,
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="openArchiveProjectsModal()"
)
i.fa.fa-trash-o
i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`)
.btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
a.btn.btn-default.dropdown-toggle(

View file

@ -41,7 +41,7 @@
li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')")
a(href) #{translate("shared_with_you")}
li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')")
a(href) #{translate("deleted_projects")}
a(href) #{settings.overleaf ? translate("archived_projects") : translate("deleted_projects")}
if isShowingV1Projects
li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')")
a(href) #{translate("v1_projects")}

View file

@ -80,6 +80,8 @@ define [
miniReviewPanelVisible: false
chatResizerSizeOpen: window.uiConfig.chatResizerSizeOpen
chatResizerSizeClosed: window.uiConfig.chatResizerSizeClosed
defaultFontFamily: window.uiConfig.defaultFontFamily
defaultLineHeight: window.uiConfig.defaultLineHeight
}
$scope.user = window.user

View file

@ -3,6 +3,7 @@ define [
"ace/ace"
"ace/ext-searchbox"
"ace/ext-modelist"
"ace/keybinding-vim"
"ide/editor/directives/aceEditor/undo/UndoManager"
"ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager"
"ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
@ -14,9 +15,10 @@ define [
"ide/graphics/services/graphics"
"ide/preamble/services/preamble"
"ide/files/services/files"
], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) ->
], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) ->
EditSession = ace.require('ace/edit_session').EditSession
ModeList = ace.require('ace/ext/modelist')
Vim = ace.require('ace/keyboard/vim').Vim
# set the path for ace workers if using a CDN (from editor.pug)
if window.aceWorkerPath != ""
@ -60,6 +62,7 @@ define [
onCtrlJ: "=" # Toggle the review panel
onCtrlShiftC: "=" # Add a new comment
onCtrlShiftA: "=" # Toggle track-changes on/off
onSave: "=" # Cmd/Ctrl-S or :w in Vim
syntaxValidation: "="
reviewPanel: "="
eventsBridge: "="
@ -67,6 +70,8 @@ define [
trackChangesEnabled: "="
docId: "="
rendererData: "="
lineHeight: "="
fontFamily: "="
}
link: (scope, element, attrs) ->
# Don't freak out if we're already in an apply callback
@ -106,16 +111,26 @@ define [
metadataManager = new MetadataManager(scope, editor, element, metadata)
autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, graphics, preamble, files)
# Prevert Ctrl|Cmd-S from triggering save dialog
editor.commands.addCommand
name: "save",
bindKey: win: "Ctrl-S", mac: "Command-S"
exec: () ->
readOnly: true
scope.$watch "onSave", (callback) ->
if callback?
Vim.defineEx 'write', 'w', callback
editor.commands.addCommand
name: "save",
bindKey: win: "Ctrl-S", mac: "Command-S"
exec: callback
readOnly: true
# Not technically 'save', but Ctrl-. recompiles in OL v1
# so maintain compatibility
editor.commands.addCommand
name: "recompile_v1",
bindKey: win: "Ctrl-.", mac: "Ctrl-."
exec: callback
readOnly: true
editor.commands.removeCommand "transposeletters"
editor.commands.removeCommand "showSettingsMenu"
editor.commands.removeCommand "foldall"
# For European keyboards, the / is above 7 so needs Shift pressing.
# This comes through as Command-Shift-/ on OS X, which is mapped to
# toggleBlockComment.
@ -266,6 +281,29 @@ define [
"font-size": value + "px"
})
scope.$watch "fontFamily", (value) ->
if value?
switch value
when 'monaco'
editor.setOption('fontFamily', '"Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace')
when 'lucida'
editor.setOption('fontFamily', '"Lucida Console", monospace')
else
editor.setOption('fontFamily', null)
scope.$watch "lineHeight", (value) ->
if value?
switch value
when 'compact'
editor.container.style.lineHeight = 1.33
when 'normal'
editor.container.style.lineHeight = 1.6
when 'wide'
editor.container.style.lineHeight = 2
else
editor.container.style.lineHeight = 1.6
editor.renderer.updateFontSize()
scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) ->
if old_sharejs_doc?
detachFromAce(old_sharejs_doc)

View file

@ -8,6 +8,12 @@ define [
if $scope.settings.pdfViewer not in ["pdfjs", "native"]
$scope.settings.pdfViewer = "pdfjs"
if $scope.settings.fontFamily? and $scope.settings.fontFamily not in ["monaco", "lucida"]
delete $scope.settings.fontFamily
if $scope.settings.lineHeight? and $scope.settings.lineHeight not in ["compact", "normal", "wide"]
delete $scope.settings.lineHeight
$scope.fontSizeAsStr = (newVal) ->
if newVal?
$scope.settings.fontSize = newVal
@ -41,6 +47,14 @@ define [
if syntaxValidation != oldSyntaxValidation
settings.saveSettings({syntaxValidation: syntaxValidation})
$scope.$watch "settings.fontFamily", (fontFamily, oldFontFamily) =>
if fontFamily != oldFontFamily
settings.saveSettings({fontFamily: fontFamily})
$scope.$watch "settings.lineHeight", (lineHeight, oldLineHeight) =>
if lineHeight != oldLineHeight
settings.saveSettings({lineHeight: lineHeight})
$scope.$watch "project.spellCheckLanguage", (language, oldLanguage) =>
return if @ignoreUpdates
if oldLanguage? and language != oldLanguage

View file

@ -8,6 +8,7 @@ define [
$scope.notifications = window.data.notifications
$scope.allSelected = false
$scope.selectedProjects = []
$scope.isArchiveableProjectSelected = false
$scope.filter = "all"
$scope.predicate = "lastUpdated"
$scope.nUntagged = 0
@ -85,6 +86,8 @@ define [
$scope.updateSelectedProjects = () ->
$scope.selectedProjects = $scope.projects.filter (project) -> project.selected
$scope.isArchiveableProjectSelected = $scope.selectedProjects.some (project) ->
window.user_id == project.owner._id
$scope.getSelectedProjects = () ->
$scope.selectedProjects

View file

@ -0,0 +1,71 @@
ace.define("ace/theme/overleaf",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
"use strict";
exports.isDark = false;
exports.cssClass = "ace-overleaf";
exports.cssText = ".ace-overleaf .ace_gutter {\
background: #f0f0f0;\
color: #333;\
}\
.ace-overleaf .ace_print-margin {\
width: 1px;\
background: #e8e8e8;\
}\
.ace-overleaf {\
background-color: #FFFFFF;\
color: black;\
}\
.ace-overleaf .ace_cursor {\
color: black;\
}\
.ace-overleaf .ace_marker-layer .ace_selection {\
background: rgb(181, 213, 255);\
}\
.ace-overleaf.ace_multiselect .ace_selection.ace_start {\
box-shadow: 0 0 3px 0px white;\
}\
.ace-overleaf .ace_marker-layer .ace_step {\
background: rgb(252, 255, 0);\
}\
.ace-overleaf .ace_marker-layer .ace_bracket {\
border: 1px solid #5A5CAD;\
}\
.ace-overleaf .ace_marker-layer .ace_active-line {\
background: rgba(0, 0, 0, 0.07);\
}\
.ace-overleaf .ace_gutter-active-line {\
background-color: #dcdcdc;\
}\
.ace-overleaf .ace_marker-layer .ace_selected-word {\
background: rgb(250, 250, 255);\
border: 1px solid rgb(200, 200, 250);\
}\
.ace-overleaf .ace_fold {\
background-color: #6B72E6;\
}\
.ace-overleaf .ace_comment {\
color: #0080FF;\
font-style: italic;\
}\
.ace-overleaf .ace_storage,\
.ace-overleaf .ace_keyword {\
color: #3F7F7F;\
}\
.ace-overleaf .ace_variable,\
.ace-overleaf .ace_string {\
color: #5A5CAD;\
}\
";
exports.$id = "ace/theme/overleaf";
var dom = require("../lib/dom");
dom.importCssString(exports.cssText, exports.cssClass);
});
(function() {
ace.require(["ace/theme/overleaf"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

View file

@ -22,6 +22,7 @@ describe "ProjectStructureMongoLock", ->
before (done) ->
# We want to instantly fail if the lock is taken
LockManager.MAX_LOCK_WAIT_TIME = 1
@lockValue = "lock-value"
userDetails =
holdingAccount:false,
email: 'test@example.com'
@ -33,11 +34,13 @@ describe "ProjectStructureMongoLock", ->
@locked_project = project
namespace = ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE
@lock_key = "lock:web:#{namespace}:#{project._id}"
LockManager._getLock @lock_key, namespace, done
LockManager._getLock @lock_key, namespace, (err, lockValue) =>
@lockValue = lockValue
done()
return
after (done) ->
LockManager._releaseLock @lock_key, done
LockManager._releaseLock @lock_key, @lockValue, done
describe 'interacting with the locked project', ->
LOCKING_UPDATE_METHODS = ['addDoc', 'addFile', 'mkdirp', 'moveEntity', 'renameEntity', 'addFolder']

View file

@ -98,7 +98,11 @@ describe 'ProjectCreationHandler', ->
it "should set the overleaf id if overleaf id provided", (done)->
overleaf_id = 2345
@handler.createBlankProject ownerId, projectName, overleaf_id, (err, project)->
attributes =
overleaf:
history:
id: overleaf_id
@handler.createBlankProject ownerId, projectName, attributes, (err, project)->
project.overleaf.history.id.should.equal overleaf_id
done()

View file

@ -872,6 +872,12 @@ describe 'ProjectEntityUpdateHandler', ->
.calledWith(@project, @entity, @path, userId)
.should.equal true
it "should should send the update to the doc updater", ->
oldDocs = [ doc: @entity, path: @path ]
@DocumentUpdaterHandler.updateProjectStructure
.calledWith(project_id, projectHistoryId, userId, {oldDocs})
.should.equal true
describe "a folder", ->
beforeEach (done) ->
@folder =
@ -905,6 +911,13 @@ describe 'ProjectEntityUpdateHandler', ->
.calledWith(@project, @doc2, "/folder/doc-name-2", userId)
.should.equal true
it "should should send one update to the doc updater for all docs and files", ->
oldFiles = [ {file: @file2, path: "/folder/file-name-2"}, {file: @file1, path: "/folder/subfolder/file-name-1"} ]
oldDocs = [ {doc: @doc2, path: "/folder/doc-name-2"}, { doc: @doc1, path: "/folder/subfolder/doc-name-1"} ]
@DocumentUpdaterHandler.updateProjectStructure
.calledWith(project_id, projectHistoryId, userId, {oldFiles, oldDocs})
.should.equal true
describe "_cleanUpDoc", ->
beforeEach ->
@doc =
@ -941,12 +954,6 @@ describe 'ProjectEntityUpdateHandler', ->
.calledWith(project_id, @doc._id.toString())
.should.equal true
it "should should send the update to the doc updater", ->
oldDocs = [ doc: @doc, path: @path ]
@DocumentUpdaterHandler.updateProjectStructure
.calledWith(project_id, projectHistoryId, userId, {oldDocs})
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true

View file

@ -3,23 +3,25 @@ assert = require('assert')
path = require('path')
modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js'
lockKey = "lock:web:{#{5678}}"
lockValue = "123456"
SandboxedModule = require('sandboxed-module')
describe 'LockManager - releasing the lock', ()->
deleteStub = sinon.stub().callsArgWith(1)
deleteStub = sinon.stub().callsArgWith(4)
mocks =
"logger-sharelatex": log:->
"./RedisWrapper":
client: ()->
auth:->
del:deleteStub
LockManager = SandboxedModule.require(modulePath, requires: mocks)
eval:deleteStub
LockManager = SandboxedModule.require(modulePath, requires: mocks)
LockManager.unlockScript = "this is the unlock script"
it 'should put a all data into memory', (done)->
LockManager._releaseLock lockKey, ->
deleteStub.calledWith(lockKey).should.equal true
LockManager._releaseLock lockKey, lockValue, ->
deleteStub.calledWith(LockManager.unlockScript, 1, lockKey, lockValue).should.equal true
done()

View file

@ -22,10 +22,11 @@ describe 'LockManager - trying the lock', ->
describe "when the lock is not set", ->
beforeEach ->
@set.callsArgWith(5, null, "OK")
@LockManager.randomLock = sinon.stub().returns("random-lock-value")
@LockManager._tryLock @key, @namespace, @callback
it "should set the lock key with an expiry if it is not set", ->
@set.calledWith(@key, "locked", "EX", 30, "NX")
@set.calledWith(@key, "random-lock-value", "EX", 30, "NX")
.should.equal true
it "should return the callback with true", ->