Merge branch 'master' into ns-no-duplicate-packages

This commit is contained in:
Nate Stemen 2018-01-05 11:46:44 -05:00
commit cf4d6c1165
80 changed files with 1533 additions and 629 deletions

View file

@ -39,6 +39,7 @@ data/*
app.js
app/js/*
test/unit/js/*
test/unit_frontend/js/*
test/smoke/js/*
test/acceptance/js/*
cookies.txt
@ -67,6 +68,11 @@ public/minjs/
Gemfile.lock
public/stylesheets/ol-style.*.css
public/stylesheets/style.*.css
public/js/libs/require*.js
*.swp
.DS_Store

View file

@ -205,12 +205,12 @@ module.exports = (grunt) ->
modules: [
{
name: "main",
exclude: ["libs"]
exclude: ["libraries"]
}, {
name: "ide",
exclude: ["libs", "pdfjs-dist/build/pdf"]
}, {
name: "libs"
exclude: ["pdfjs-dist/build/pdf", "libraries"]
},{
name: "libraries"
},{
name: "ace/mode-latex"
},{

View file

@ -60,7 +60,7 @@ pipeline {
sh 'git config --global core.logallrefupdates false'
sh 'mv app/views/external/robots.txt public/robots.txt'
sh 'mv app/views/external/googlebdb0f8f7f4a17241.html public/googlebdb0f8f7f4a17241.html'
sh 'npm install'
sh 'npm --quiet install'
sh 'npm rebuild'
// It's too easy to end up shrinkwrapping to an outdated version of translations.
// Ensure translations are always latest, regardless of shrinkwrap
@ -71,16 +71,9 @@ pipeline {
}
}
stage('Unit Tests') {
stage('Test') {
steps {
sh 'make clean install' // Removes js files, so do before compile
sh 'make test_unit MOCHA_ARGS="--reporter=tap"'
}
}
stage('Acceptance Tests') {
steps {
sh 'make test_acceptance MOCHA_ARGS="--reporter=tap"'
sh 'make ci'
}
}
@ -155,6 +148,10 @@ pipeline {
}
post {
always {
sh 'make ci_clean'
}
failure {
mail(from: "${EMAIL_ALERT_FROM}",
to: "${EMAIL_ALERT_TO}",

View file

@ -16,9 +16,10 @@ add_dev: docker-shared.yml
$(NPM) install --save-dev ${P}
install: docker-shared.yml
bin/generate_volumes_file
$(NPM) install
clean:
clean: ci_clean
rm -f app.js
rm -rf app/js
rm -rf test/unit/js
@ -30,9 +31,8 @@ clean:
rm -rf $$dir/test/unit/js; \
rm -rf $$dir/test/acceptance/js; \
done
# Regenerate docker-shared.yml - not stictly a 'clean',
# but lets `make clean install` work nicely
bin/generate_volumes_file
ci_clean:
# Deletes node_modules volume
docker-compose down --volumes
@ -40,11 +40,14 @@ clean:
docker-shared.yml:
bin/generate_volumes_file
test: test_unit test_acceptance
test: test_unit test_frontend test_acceptance
test_unit: docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:unit -- ${MOCHA_ARGS}
test_frontend: docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:frontend -- ${MOCHA_ARGS}
test_acceptance: test_acceptance_app test_acceptance_modules
test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run test_acceptance_app_stop_service
@ -71,7 +74,11 @@ test_acceptance_modules: docker-shared.yml
test_acceptance_module: docker-shared.yml
cd $(MODULE) && make test_acceptance
ci:
MOCHA_ARGS="--reporter tap" \
$(MAKE) install test
.PHONY:
all add install update test test_unit test_acceptance \
all add install update test test_unit test_frontend test_acceptance \
test_acceptance_start_service test_acceptance_stop_service \
test_acceptance_run
test_acceptance_run ci ci_clean

View file

@ -205,10 +205,10 @@ module.exports = DocumentUpdaterHandler =
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
updateProjectStructure : (project_id, userId, changes, callback = (error) ->)->
return callback() if !settings.apis.project_history?.enabled
return callback() if !settings.apis.project_history?.sendProjectStructureOps
docUpdates = DocumentUpdaterHandler._getRenameUpdates('doc', changes.oldDocs, changes.newDocs)
fileUpdates = DocumentUpdaterHandler._getRenameUpdates('file', changes.oldFiles, changes.newFiles)
docUpdates = DocumentUpdaterHandler._getUpdates('doc', changes.oldDocs, changes.newDocs)
fileUpdates = DocumentUpdaterHandler._getUpdates('file', changes.oldFiles, changes.newFiles)
timer = new metrics.Timer("set-document")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}"
@ -230,7 +230,7 @@ module.exports = DocumentUpdaterHandler =
logger.error {project_id, url}, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
_getRenameUpdates: (entityType, oldEntities, newEntities) ->
_getUpdates: (entityType, oldEntities, newEntities) ->
oldEntities ||= []
newEntities ||= []
updates = []
@ -255,6 +255,16 @@ module.exports = DocumentUpdaterHandler =
pathname: oldEntity.path
newPathname: newEntity.path
for id, oldEntity of oldEntitiesHash
newEntity = newEntitiesHash[id]
if !newEntity?
# entity deleted
updates.push
id: id
pathname: oldEntity.path
newPathname: ''
updates
PENDINGUPDATESKEY = "PendingUpdates"

View file

@ -105,19 +105,19 @@ module.exports = EditorController =
async.series jobs, (err)->
callback err, newFolders, lastFolder
deleteEntity : (project_id, entity_id, entityType, source, callback)->
deleteEntity : (project_id, entity_id, entityType, source, userId, callback)->
LockManager.getLock project_id, (err)->
if err?
logger.err err:err, project_id:project_id, "could not get lock to deleteEntity"
return callback(err)
EditorController.deleteEntityWithoutLock project_id, entity_id, entityType, source, (err)->
EditorController.deleteEntityWithoutLock project_id, entity_id, entityType, source, userId, (err)->
LockManager.releaseLock project_id, ()->
callback(err)
deleteEntityWithoutLock: (project_id, entity_id, entityType, source, callback)->
deleteEntityWithoutLock: (project_id, entity_id, entityType, source, userId, callback)->
logger.log {project_id, entity_id, entityType, source}, "start delete process of entity"
Metrics.inc "editor.delete-entity"
ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, (err)->
ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, userId, (err)->
if err?
logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, "error deleting entity"
return callback(err)

View file

@ -147,6 +147,7 @@ module.exports = EditorHttpController =
project_id = req.params.Project_id
entity_id = req.params.entity_id
entity_type = req.params.entity_type
EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) ->
user_id = AuthenticationController.getLoggedInUserId(req)
EditorController.deleteEntity project_id, entity_id, entity_type, "editor", user_id, (error) ->
return next(error) if error?
res.sendStatus 204

View file

@ -26,8 +26,16 @@ InvalidNameError = (message) ->
return error
InvalidNameError.prototype.__proto__ = Error.prototype
UnsupportedFileTypeError = (message) ->
error = new Error(message)
error.name = "UnsupportedFileTypeError"
error.__proto__ = UnsupportedFileTypeError.prototype
return error
UnsupportedFileTypeError.prototype.__proto___ = Error.prototype
module.exports = Errors =
NotFoundError: NotFoundError
ServiceNotConfiguredError: ServiceNotConfiguredError
TooManyRequestsError: TooManyRequestsError
InvalidNameError: InvalidNameError
UnsupportedFileTypeError: UnsupportedFileTypeError

View file

@ -5,35 +5,13 @@ AuthenticationController = require "../Authentication/AuthenticationController"
ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
module.exports = HistoryController =
initializeProject: (callback = (error, history_id) ->) ->
return callback() if !settings.apis.project_history?.enabled
request.post {
url: "#{settings.apis.project_history.url}/project"
}, (error, res, body)->
return callback(error) if error?
if res.statusCode >= 200 and res.statusCode < 300
try
project = JSON.parse(body)
catch error
return callback(error)
overleaf_id = project?.project?.id
if !overleaf_id
error = new Error("project-history did not provide an id", project)
return callback(error)
callback null, { overleaf_id }
else
error = new Error("project-history returned a non-success status code: #{res.statusCode}")
callback error
selectHistoryApi: (req, res, next = (error) ->) ->
project_id = req.params?.Project_id
# find out which type of history service this project uses
ProjectDetailsHandler.getDetails project_id, (err, project) ->
return next(err) if err?
if project?.overleaf?.history?.display
history = project.overleaf?.history
if history?.id? and history?.display
req.useProjectHistory = true
else
req.useProjectHistory = false
@ -58,7 +36,7 @@ module.exports = HistoryController =
buildHistoryServiceUrl: (useProjectHistory) ->
# choose a history service, either document-level (trackchanges)
# or project-level (project_history)
if settings.apis.project_history?.enabled && useProjectHistory
if useProjectHistory
return settings.apis.project_history.url
else
return settings.apis.trackchanges.url

View file

@ -0,0 +1,26 @@
request = require "request"
settings = require "settings-sharelatex"
module.exports = HistoryManager =
initializeProject: (callback = (error, history_id) ->) ->
return callback() if !settings.apis.project_history?.initializeHistoryForNewProjects
request.post {
url: "#{settings.apis.project_history.url}/project"
}, (error, res, body)->
return callback(error) if error?
if res.statusCode >= 200 and res.statusCode < 300
try
project = JSON.parse(body)
catch error
return callback(error)
overleaf_id = project?.project?.id
if !overleaf_id
error = new Error("project-history did not provide an id", project)
return callback(error)
callback null, { overleaf_id }
else
error = new Error("project-history returned a non-success status code: #{res.statusCode}")
callback error

View file

@ -216,7 +216,7 @@ module.exports = ProjectController =
project: (cb)->
ProjectGetter.getProject(
project_id,
{ name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1 },
{ name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1, 'overleaf.history.display': 1 },
cb
)
user: (cb)->
@ -253,7 +253,7 @@ module.exports = ProjectController =
# Extract data from user's ObjectId
timestamp = parseInt(user_id.toString().substring(0, 8), 16)
rolloutPercentage = 40 # Percentage of users to roll out to
rolloutPercentage = 60 # Percentage of users to roll out to
if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage)
# Don't show if user is not part of roll out
return cb(null, { enabled: false, showOnboarding: false })
@ -351,6 +351,7 @@ module.exports = ProjectController =
themes: THEME_LIST
maxDocLength: Settings.max_doc_length
showLinkSharingOnboarding: !!results.couldShowLinkSharingOnboarding
useV2History: !!project.overleaf?.history?.display
timer.done()
_buildProjectList: (allProjects, v1Projects = [])->

View file

@ -7,7 +7,7 @@ Project = require('../../models/Project').Project
Folder = require('../../models/Folder').Folder
ProjectEntityHandler = require('./ProjectEntityHandler')
ProjectDetailsHandler = require('./ProjectDetailsHandler')
HistoryController = require('../History/HistoryController')
HistoryManager = require('../History/HistoryManager')
User = require('../../models/User').User
fs = require('fs')
Path = require "path"
@ -27,7 +27,7 @@ module.exports = ProjectCreationHandler =
if projectHistoryId?
ProjectCreationHandler._createBlankProject owner_id, projectName, projectHistoryId, callback
else
HistoryController.initializeProject (error, history) ->
HistoryManager.initializeProject (error, history) ->
return callback(error) if error?
ProjectCreationHandler._createBlankProject owner_id, projectName, history?.overleaf_id, callback

View file

@ -21,7 +21,7 @@ module.exports = ProjectDuplicator =
if !doc?._id?
return callback()
content = docContents[doc._id.toString()]
projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)->
projectEntityHandler.addDoc newProject, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)->
if err?
logger.err err:err, "error copying doc"
return callback(err)

View file

@ -149,14 +149,35 @@ module.exports = ProjectEntityHandler =
else
DocstoreManager.getDoc project_id, doc_id, options, callback
addDoc: (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
addDoc: (project_or_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
ProjectEntityHandler.addDocWithoutUpdatingHistory project_or_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path) ->
return callback(error) if error?
newDocs = [
doc: doc
path: path
docLines: docLines.join('\n')
]
project_id = project_or_id._id or project_or_id
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) ->
return callback(error) if error?
callback null, doc, folder_id
addDocWithoutUpdatingHistory: (project_or_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
# This method should never be called directly, except when importing a project
# from Overleaf. It skips sending updates to the project history, which will break
# the history unless you are making sure it is updated in some other way.
getProject = (cb) ->
if project_or_id._id? # project
return cb(null, project_or_id)
else # id
return ProjectGetter.getProjectWithOnlyFolders project_or_id, cb
getProject (error, project) ->
if err?
logger.err project_id:project_id, err:err, "error getting project for add doc"
return callback(err)
ProjectEntityHandler.addDocWithProject project, folder_id, docName, docLines, userId, callback
ProjectEntityHandler._addDocWithProject project, folder_id, docName, docLines, userId, callback
addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
_addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id, path) ->)=>
project_id = project._id
logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project"
confirmFolder project, folder_id, (folder_id)=>
@ -176,14 +197,7 @@ module.exports = ProjectEntityHandler =
rev: 0
}, (err) ->
return callback(err) if err?
newDocs = [
doc: doc
path: result?.path?.fileSystem
docLines: docLines.join('\n')
]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) ->
return callback(error) if error?
callback null, doc, folder_id
callback(null, doc, folder_id, result?.path?.fileSystem)
restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
# getDoc will return the deleted doc's lines, but we don't actually remove
@ -192,37 +206,37 @@ module.exports = ProjectEntityHandler =
return callback(error) if error?
ProjectEntityHandler.addDoc project_id, null, name, lines, callback
addFile: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)->
addFileWithoutUpdatingHistory: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)->
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
if err?
logger.err project_id:project_id, err:err, "error getting project for add file"
return callback(err)
ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, userId, callback
addFileWithProject: (project, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)->
project_id = project._id
logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)->
fileRef = new File name : fileName
FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err, fileStoreUrl)->
if err?
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
return callback(err)
ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)->
fileRef = new File name : fileName
FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err, fileStoreUrl)->
if err?
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project"
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
return callback(err)
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) ->
return callback(err) if err?
newFiles = [
file: fileRef
path: result?.path?.fileSystem
url: fileStoreUrl
]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
return callback(error) if error?
callback null, fileRef, folder_id
ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
if err?
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project"
return callback(err)
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) ->
return callback(err) if err?
callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl)
addFile: (project_id, folder_id, fileName, fsPath, userId, callback = (error, fileRef, folder_id) ->)->
ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, fsPath, userId, (error, fileRef, folder_id, path, fileStoreUrl) ->
newFiles = [
file: fileRef
path: path
url: fileStoreUrl
]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
return callback(error) if error?
callback null, fileRef, folder_id
replaceFile: (project_id, file_id, fsPath, userId, callback)->
self = ProjectEntityHandler
@ -412,7 +426,7 @@ module.exports = ProjectEntityHandler =
callback()
deleteEntity: (project_id, entity_id, entityType, callback = (error) ->)->
deleteEntity: (project_id, entity_id, entityType, userId, callback = (error) ->)->
self = @
logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity"
if !entityType?
@ -423,7 +437,7 @@ module.exports = ProjectEntityHandler =
return callback(error) if error?
projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=>
return callback(error) if error?
ProjectEntityHandler._cleanUpEntity project, entity, entityType, (error) ->
ProjectEntityHandler._cleanUpEntity project, entity, entityType, path.fileSystem, userId, (error) ->
return callback(error) if error?
tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, (error) ->
return callback(error) if error?
@ -456,17 +470,17 @@ module.exports = ProjectEntityHandler =
return callback(error) if error?
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback
_cleanUpEntity: (project, entity, entityType, callback = (error) ->) ->
_cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) ->
if(entityType.indexOf("file") != -1)
ProjectEntityHandler._cleanUpFile project, entity, callback
ProjectEntityHandler._cleanUpFile project, entity, path, userId, callback
else if (entityType.indexOf("doc") != -1)
ProjectEntityHandler._cleanUpDoc project, entity, callback
ProjectEntityHandler._cleanUpDoc project, entity, path, userId, callback
else if (entityType.indexOf("folder") != -1)
ProjectEntityHandler._cleanUpFolder project, entity, callback
ProjectEntityHandler._cleanUpFolder project, entity, path, userId, callback
else
callback()
_cleanUpDoc: (project, doc, callback = (error) ->) ->
_cleanUpDoc: (project, doc, path, userId, callback = (error) ->) ->
project_id = project._id.toString()
doc_id = doc._id.toString()
unsetRootDocIfRequired = (callback) =>
@ -483,26 +497,33 @@ module.exports = ProjectEntityHandler =
return callback(error) if error?
DocstoreManager.deleteDoc project_id, doc_id, (error) ->
return callback(error) if error?
callback()
changes = oldDocs: [ {doc, path} ]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
_cleanUpFile: (project, file, callback = (error) ->) ->
_cleanUpFile: (project, file, path, userId, callback = (error) ->) ->
project_id = project._id.toString()
file_id = file._id.toString()
FileStoreHandler.deleteFile project_id, file_id, callback
FileStoreHandler.deleteFile project_id, file_id, (error) ->
return callback(error) if error?
changes = oldFiles: [ {file, path} ]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
_cleanUpFolder: (project, folder, callback = (error) ->) ->
_cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) ->
jobs = []
for doc in folder.docs
do (doc) ->
jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, callback
docPath = path.join(folderPath, doc.name)
jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, docPath, userId, callback
for file in folder.fileRefs
do (file) ->
jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, callback
filePath = path.join(folderPath, file.name)
jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, filePath, userId, callback
for childFolder in folder.folders
do (childFolder) ->
jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, callback
folderPath = path.join(folderPath, childFolder.name)
jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, folderPath, userId, callback
async.series jobs, callback

View file

@ -62,6 +62,9 @@ module.exports = SubscriptionUpdater =
invited_emails: email
}, callback
refreshSubscription: (user_id, callback=(err)->) ->
SubscriptionUpdater._setUsersMinimumFeatures user_id, callback
deleteSubscription: (subscription_id, callback = (error) ->) ->
SubscriptionLocator.getSubscription subscription_id, (err, subscription) ->
return callback(err) if err?
@ -106,17 +109,29 @@ module.exports = SubscriptionUpdater =
SubscriptionLocator.getUsersSubscription user_id, cb
groupSubscription: (cb)->
SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb
v1PlanCode: (cb) ->
Modules = require '../../infrastructure/Modules'
Modules.hooks.fire 'getV1PlanCode', user_id, (err, results) ->
cb(err, results?[0] || null)
async.series jobs, (err, results)->
if err?
logger.err err:err, user_id:user, "error getting subscription or group for _setUsersMinimumFeatures"
logger.err err:err, user_id:user_id,
"error getting subscription or group for _setUsersMinimumFeatures"
return callback(err)
{subscription, groupSubscription} = results
if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode
logger.log user_id:user_id, "using users subscription plan code for features"
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
else if groupSubscription? and groupSubscription.planCode?
{subscription, groupSubscription, v1PlanCode} = results
# Group Subscription
if groupSubscription? and groupSubscription.planCode?
logger.log user_id:user_id, "using group which user is memor of for features"
UserFeaturesUpdater.updateFeatures user_id, groupSubscription.planCode, callback
# Personal Subscription
else if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode
logger.log user_id:user_id, "using users subscription plan code for features"
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
# V1 Subscription
else if v1PlanCode?
logger.log user_id: user_id, "using the V1 plan for features"
UserFeaturesUpdater.updateFeatures user_id, v1PlanCode, callback
# Default
else
logger.log user_id:user_id, "using default features for user with no subscription or group"
UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, (err)->

View file

@ -47,7 +47,7 @@ module.exports =
logger.log user_id:user_id, filePath:path, projectName:projectName, project_id:project._id, "project found for delete update, path is root so marking project as deleted"
return projectDeleter.markAsDeletedByExternalSource project._id, callback
else
updateMerger.deleteUpdate project._id, path, source, (err)->
updateMerger.deleteUpdate user_id, project._id, path, source, (err)->
callback(err)

View file

@ -32,13 +32,13 @@ module.exports =
else
self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback
deleteUpdate: (project_id, path, source, callback)->
deleteUpdate: (user_id, project_id, path, source, callback)->
projectLocator.findElementByPath project_id, path, (err, element, type)->
if err? || !element?
logger.log element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted"
return callback()
logger.log project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds"
editorController.deleteEntity project_id, element._id, type, source, (err)->
editorController.deleteEntity project_id, element._id, type, source, user_id, (err)->
logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds"
callback()

View file

@ -12,7 +12,7 @@ Modules = require "./Modules"
Url = require "url"
PackageVersions = require "./PackageVersions"
htmlEncoder = new require("node-html-encoder").Encoder("numerical")
fingerprints = {}
hashedFiles = {}
Path = require 'path'
Features = require "./Features"
@ -30,43 +30,42 @@ getFileContent = (filePath)->
filePath = Path.join __dirname, "../../../", "public#{filePath}"
exists = fs.existsSync filePath
if exists
content = fs.readFileSync filePath
content = fs.readFileSync filePath, "UTF-8"
return content
else
logger.log filePath:filePath, "file does not exist for fingerprints"
logger.log filePath:filePath, "file does not exist for hashing"
return ""
logger.log "Generating file fingerprints..."
pathList = [
["#{jsPath}libs/#{fineuploader}.js"]
["#{jsPath}libs/require.js"]
["#{jsPath}ide.js"]
["#{jsPath}main.js"]
["#{jsPath}libs.js"]
["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js","#{jsPath}#{ace}/worker-latex.js","#{jsPath}#{ace}/snippets/latex.js"]
["#{jsPath}libs/#{pdfjs}/pdf.js"]
["#{jsPath}libs/#{pdfjs}/pdf.worker.js"]
["#{jsPath}libs/#{pdfjs}/compatibility.js"]
["/stylesheets/style.css"]
["/stylesheets/ol-style.css"]
"#{jsPath}libs/require.js"
"#{jsPath}ide.js"
"#{jsPath}main.js"
"#{jsPath}libraries.js"
"/stylesheets/style.css"
"/stylesheets/ol-style.css"
]
for paths in pathList
contentList = _.map(paths, getFileContent)
content = contentList.join("")
hash = crypto.createHash("md5").update(content).digest("hex")
_.each paths, (filePath)->
logger.log "#{filePath}: #{hash}"
fingerprints[filePath] = hash
if !Settings.useMinifiedJs
logger.log "not using minified JS, not hashing static files"
else
logger.log "Generating file hashes..."
for path in pathList
content = getFileContent(path)
hash = crypto.createHash("md5").update(content).digest("hex")
splitPath = path.split("/")
filenameSplit = splitPath.pop().split(".")
filenameSplit.splice(filenameSplit.length-1, 0, hash)
splitPath.push(filenameSplit.join("."))
getFingerprint = (path) ->
if fingerprints[path]?
return fingerprints[path]
else
logger.err "No fingerprint for file: #{path}"
return ""
hashPath = splitPath.join("/")
hashedFiles[path] = hashPath
logger.log "Finished generating file fingerprints"
fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}"
fs.writeFileSync(fsHashPath, content)
logger.log "Finished hashing static content"
cdnAvailable = Settings.cdn?.web?.host?
darkCdnAvailable = Settings.cdn?.web?.darkHost?
@ -120,29 +119,35 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
res.locals.lib = PackageVersions.lib
res.locals.buildJsPath = (jsFile, opts = {})->
path = Path.join(jsPath, jsFile)
doFingerPrint = opts.fingerprint != false
if opts.hashedPath && hashedFiles[path]?
path = hashedFiles[path]
if !opts.qs?
opts.qs = {}
if !opts.qs?.fingerprint? and doFingerPrint
opts.qs.fingerprint = getFingerprint(path)
if opts.cdn != false
path = Url.resolve(staticFilesBase, path)
qs = querystring.stringify(opts.qs)
if opts.removeExtension == true
path = path.slice(0,-3)
if qs? and qs.length > 0
path = path + "?" + qs
return path
res.locals.buildCssPath = (cssFile)->
res.locals.buildCssPath = (cssFile, opts)->
path = Path.join("/stylesheets/", cssFile)
return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path)
if opts?.hashedPath && hashedFiles[path]?
hashedPath = hashedFiles[path]
return Url.resolve(staticFilesBase, hashedPath)
return Url.resolve(staticFilesBase, path)
res.locals.buildImgPath = (imgFile)->
path = Path.join("/img/", imgFile)
@ -227,10 +232,6 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
return req.query?[field]
next()
webRouter.use (req, res, next)->
res.locals.fingerprint = getFingerprint
next()
webRouter.use (req, res, next)->
res.locals.formatPrice = SubscriptionFormatters.formatPrice
next()
@ -296,10 +297,14 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
webRouter.use (req, res, next) ->
isOl = (Settings.brandPrefix == 'ol-')
res.locals.uiConfig =
defaultResizerSizeOpen : if isOl then 2 else 24
defaultResizerSizeClosed : if isOl then 2 else 24
eastResizerCursor : if isOl then "ew-resize" else null
westResizerCursor : if isOl then "ew-resize" else null
chatResizerSizeOpen : if isOl then 2 else 12
chatResizerSizeClosed : 0
defaultResizerSizeOpen : if isOl then 2 else 24
defaultResizerSizeClosed : if isOl then 2 else 24
eastResizerCursor : if isOl then "ew-resize" else null
westResizerCursor : if isOl then "ew-resize" else null
chatResizerSizeOpen : if isOl then 2 else 12
chatResizerSizeClosed : 0
chatMessageBorderSaturation: if isOl then "85%" else "70%"
chatMessageBorderLightness : if isOl then "40%" else "70%"
chatMessageBgSaturation : if isOl then "85%" else "60%"
chatMessageBgLightness : if isOl then "40%" else "97%"
next()

View file

@ -56,6 +56,7 @@ ProjectSchema = new Schema
read_token : { type: String }
history :
id : { type: Number }
display : { type: Boolean }
ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
if project_or_id._id?

View file

@ -197,6 +197,7 @@ module.exports = class Router
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
@ -324,6 +325,10 @@ module.exports = class Router
headers: req.headers
})
webRouter.get "/no-cache", (req, res, next)->
res.header("Cache-Control", "max-age=0")
res.sendStatus(404)
webRouter.get '/oops-express', (req, res, next) -> next(new Error("Test error"))
webRouter.get '/oops-internal', (req, res, next) -> throw new Error("Test error")
webRouter.get '/oops-mongo', (req, res, next) ->

View file

@ -23,7 +23,7 @@ html(itemscope, itemtype='http://schema.org/Product')
link(rel="apple-touch-icon-precomposed", href="/" + settings.brandPrefix + "apple-touch-icon-precomposed.png")
link(rel="mask-icon", href="/" + settings.brandPrefix + "mask-favicon.svg", color="#a93529")
link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css"))
link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css", {hashedPath:true}))
block _headLinks
@ -57,7 +57,7 @@ html(itemscope, itemtype='http://schema.org/Product')
script(type="text/javascript").
window.csrfToken = "#{csrfToken}";
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
script(src=buildJsPath("libs/jquery-1.11.1.min.js"))
script(type="text/javascript").
var noCdnKey = "nocdn=true"
var cdnBlocked = typeof jQuery === 'undefined'
@ -68,7 +68,7 @@ html(itemscope, itemtype='http://schema.org/Product')
block scripts
script(src=buildJsPath("libs/angular-1.6.4.min.js", {fingerprint:false}))
script(src=buildJsPath("libs/angular-1.6.4.min.js"))
script.
window.sharelatex = {
@ -95,7 +95,11 @@ html(itemscope, itemtype='http://schema.org/Product')
cdnDomain : '!{settings.templates.cdnDomain}',
indexName : '!{settings.templates.indexName}'
}
- if (settings.overleaf && settings.overleaf.useOLFreeTrial)
script.
window.redirectToOLFreeTrialUrl = '!{settings.overleaf.host}/users/trial'
body
if(settings.recaptcha)
script(src="https://www.google.com/recaptcha/api.js?render=explicit")
@ -142,9 +146,10 @@ html(itemscope, itemtype='http://schema.org/Product')
window.requirejs = {
"paths" : {
"moment": "libs/#{lib('moment')}",
"fineuploader": "libs/#{lib('fineuploader')}"
"fineuploader": "libs/#{lib('fineuploader')}",
"main": "#{buildJsPath('main.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
"libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
},
"urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
"config":{
"moment":{
"noGlobal": true
@ -152,9 +157,9 @@ html(itemscope, itemtype='http://schema.org/Product')
}
};
script(
data-main=buildJsPath('main.js', {fingerprint:false}),
data-main=buildJsPath('main.js', {hashedPath:false}),
baseurl=fullJsPath,
src=buildJsPath('libs/require.js')
src=buildJsPath('libs/require.js', {hashedPath:true})
)
include contact-us-modal

View file

@ -98,14 +98,14 @@ block requirejs
script(type="text/javascript" src='/socket.io/socket.io.js')
//- don't use cdn for workers
- var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false,fingerprint:false})
- var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false,fingerprint:false})
- var pdfCMapsPath = buildJsPath('/libs/' + lib('pdfjs') + '/bcmaps/', {cdn:false,fingerprint:false})
- var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false})
- var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false})
- var pdfCMapsPath = buildJsPath('/libs/' + lib('pdfjs') + '/bcmaps/', {cdn:false})
//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
//- and doesn't prematurely end the script tag.
script#data(type="application/json").
!{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState}).replace(/\//g, '\\/')}
!{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState, useV2History: useV2History}).replace(/\//g, '\\/')}
script(type="text/javascript").
window.data = JSON.parse($("#data").text());
@ -126,14 +126,15 @@ block requirejs
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.requirejs = {
"paths" : {
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'}})}",
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, qs:{config:'TeX-AMS_HTML'}})}",
"moment": "libs/#{lib('moment')}",
"pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf",
"pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}",
"ace": "#{lib('ace')}",
"fineuploader": "libs/#{lib('fineuploader')}"
"fineuploader": "libs/#{lib('fineuploader')}",
"ide": "#{buildJsPath('ide.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
"libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
},
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
"waitSeconds": 0,
"shim": {
"pdfjs-dist/build/pdf": {
@ -155,14 +156,13 @@ block requirejs
}
}
};
window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}"
window.aceWorkerPath = "#{aceWorkerPath}";
window.pdfCMapsPath = "#{pdfCMapsPath}"
window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}');
script(
data-main=buildJsPath("ide.js", {fingerprint:false}),
data-main=buildJsPath("ide.js", {hashedPath:false}),
baseurl=fullJsPath,
data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}),
src=buildJsPath('libs/require.js')
data-ace-base=buildJsPath(lib('ace')),
src=buildJsPath('libs/require.js', {hashedPath:true})
)

View file

@ -35,12 +35,9 @@ aside.chat(
span(ng-if="message.user.first_name") {{ message.user.first_name }}
span(ng-if="!message.user.first_name") {{ message.user.email }}
.message(
ng-style="{\
'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)',\
'background-color': 'hsl({{ hue(message.user) }}, 60%, 97%)'\
}"
ng-style="getMessageStyle(message.user);"
)
.arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)'}")
.arrow(ng-style="getArrowStyle(message.user)")
.message-content
p(
mathjax,

View file

@ -134,8 +134,17 @@ div#history(ng-show="ui.view == 'history'")
div.description(ng-click="select()")
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
div.docs(ng-repeat="(doc_id, doc) in update.docs")
span.doc {{ doc.entity.name }}
div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
| Edited
div.docs(ng-repeat="pathname in update.pathnames")
.doc {{ pathname }}
div.docs(ng-repeat="project_op in update.project_ops")
div(ng-if="project_op.rename")
.action Renamed
.doc {{ project_op.rename.pathname }} &rarr; {{ project_op.rename.newPathname }}
div(ng-if="project_op.add")
.action Created
.doc {{ project_op.add.pathname }}
div.users
div.user(ng-repeat="update_user in update.meta.users")
.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
@ -165,8 +174,8 @@ div#history(ng-show="ui.view == 'history'")
'other': 'changes'\
}"
)
| in <strong>{{history.diff.doc.name}}</strong>
.toolbar-right
| in <strong>{{history.diff.pathname}}</strong>
.toolbar-right(ng-if="!history.isV2")
a.btn.btn-danger.btn-sm(
href,
ng-click="openRestoreDiffModal()"

View file

@ -63,6 +63,14 @@ block content
aside.project-list-sidebar.col-md-2.col-xs-3
include ./list/side-bar
if isShowingV1Projects && settings.overleaf && settings.overleaf.host
.project-list-sidebar-v2-pane.col-md-2.col-xs-3
span Welcome to the Overleaf v2 alpha! #[a(href="https://www.overleaf.com/help/v2") Find out more].
span To tag or rename your v1 projects, please go back to Overleaf v1.
a.project-list-sidebar-v1-link(
href=settings.overleaf.host + "/dash?prefer-v1-dash=1"
) Go back to v1
.project-list-main.col-md-10.col-xs-9
include ./list/notifications
include ./list/project-list

View file

@ -2,6 +2,7 @@
input.select-item(
select-individual,
type="checkbox",
ng-disabled="shouldDisableCheckbox(project)",
ng-model="project.selected"
stop-propagation="click"
aria-label=translate('select_project') + " '{{ project.name }}'"

View file

@ -317,38 +317,56 @@ script(type="text/ng-template", id="userProfileModalTemplate")
script(type="text/ng-template", id="v1ImportModalTemplate")
.modal-header
button.close(ng-click="dismiss()") &times;
h3 #{translate("import_project_to_v2")}
h3 Move Project to Overleaf v2
.modal-body.v1-import-wrapper
.v1-import-step-1(ng-show="step === 1")
img.v1-import-img(
src="/img/v1-import/v2-editor.png"
alt="The new V2 Editor."
)
h2.v1-import-title Try importing your project to V2!
p Some exciting copy about the new features:
ul
li Some stuff
li Some more stuff
li Yet more stuff
.v1-import-row
.v1-import-col
img.v1-import-img(
src="/img/v1-import/v2-editor.png"
alt="The new V2 Editor."
)
.v1-import-col
h2.v1-import-title Try the Overleaf v2 Editor
p The Overleaf v2 editor has many great new features including:
ul
li Faster real-time collaboration
li See your coauthors cursors
li Chat with math support
li Tracked changes and commenting
li Improved LaTeX autocomplete
li Two-way Dropbox sync
p.v1-import-cta Would you like to move #[strong {{project.name}}] into Overleaf v2?
.v1-import-step-2(ng-show="step === 2")
div.v1-import-warning(aria-label="Warning symbol.")
i.fa.fa-exclamation-triangle
h2.v1-import-title #[strong Warning:] Overleaf V2 is in beta
p Once importing your project you will lose access to the some of the features of Overleaf V1. This includes the git bridge, journal integrations, WYSIWYG and linked files. Were working on bringing these features to V2!
p Once you have imported a project to V2 you #[strong cannot go back to V1].
p Are you sure you want to import to V2?
.v1-import-row
.v1-import-warning.v1-import-col(aria-label="Warning symbol.")
i.fa.fa-exclamation-triangle
.v1-import-col
h2.v1-import-title #[strong Warning:] Overleaf v2 is Experimental
p We are still working hard to bring some Overleaf v1 features to the v2 editor. If you move this project to v2 now, you will:
ul
li Lose access your project via git
li Not be able to use the Journals and Services menu to submit directly to our partners
li Not be able to use the Rich Text (WYSIWYG) mode
li Not be able to use linked files (to URLs or to files in other Overleaf projects)
li Not be able to use some bibliography integrations (Zotero, CiteULike)
li Lose access to your labelled versions and not be able to create new labelled versions
.v1-import-cta
p
strong Please note: you cannot move this project back to v1 once you have moved it to v2. If this is an important project, please consider making a clone in v1 before you move the project to v2.
p Are you sure you want to move #[strong {{project.name}}] into Overleaf v2?
.modal-footer.v1-import-footer
div(ng-show="step === 1")
if settings.overleaf && settings.overleaf.host
a.btn.btn-primary.v1-import-btn(
ng-href=settings.overleaf.host + "/{{project.id}}"
) #{translate("open_in_v1")}
) No thanks, open in v1
button.btn.btn-primary.v1-import-btn(
ng-click="moveToConfirmation()"
) #{translate("import_to_v2")}
) Yes, move project to v2
div(ng-show="step === 2")
form(
async-form="v1Import",
@ -363,9 +381,9 @@ script(type="text/ng-template", id="v1ImportModalTemplate")
a.btn.btn-primary.v1-import-btn(
ng-href=settings.overleaf.host + "/{{project.id}}"
ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.success}"
) #{translate("never_mind_open_in_v1")}
) No thanks, open in v1
input.btn.btn-primary.v1-import-btn(
type="submit",
value=translate('yes_im_sure')
value="Yes, move project to v2"
ng-disabled="v1ImportForm.inflight || v1ImportForm.success"
)

View file

@ -210,7 +210,7 @@ block content
h3 #{translate("group_plan_enquiry")}
.modal-body
form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak)
span(ng-show="sent == false")
span(ng-show="sent == false && error == false")
.form-group
label#title9(for='Field9')
| Name
@ -228,11 +228,13 @@ block content
.form-group
input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
.form-group
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'ShareLaTeX for Universities';")
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';")
.form-group.text-center
input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote')
span(ng-show="sent")
span(ng-show="sent == true && error == false")
p Request Sent, Thank you.
span(ng-show="error")
p Error sending request.
.row
.col-md-12

View file

@ -1,6 +1,6 @@
script(type="text/ng-template", id="v1ProjectTooltipTemplate")
span This project is from Overleaf version 1 and has not been imported to the beta yet
span This project is from Overleaf v1 and has not been imported to v2 yet.
script(type="text/ng-template", id="v1TagTooltipTemplate")
span This folder is from Overleaf version 1 and has not been imported to the beta yet
span This folder/tag is from Overleaf v1. Please go back to v1 to manage.

View file

@ -0,0 +1,5 @@
#!/bin/bash
set -e;
COFFEE=node_modules/.bin/coffee
echo Compiling public/coffee;
$COFFEE -o public/js -c public/coffee;

View file

@ -0,0 +1,5 @@
#!/bin/bash
set -e;
COFFEE=node_modules/.bin/coffee
echo Compiling test/unit_frontend/coffee;
$COFFEE -o test/unit_frontend/js -c test/unit_frontend/coffee;

5
services/web/bin/frontend_test Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
set -e;
MOCHA="node_modules/.bin/mocha --recursive --reporter spec"
$MOCHA "$@" test/unit_frontend/js

View file

@ -111,7 +111,8 @@ module.exports = settings =
trackchanges:
url : "http://localhost:3015"
project_history:
enabled: process.env.PROJECT_HISTORY_ENABLED == 'true' or false
sendProjectStructureOps: process.env.PROJECT_HISTORY_ENABLED == 'true' or false
initializeHistoryForNewProjects: process.env.PROJECT_HISTORY_ENABLED == 'true' or false
url : "http://localhost:3054"
docstore:
url : "http://#{process.env['DOCSTORE_HOST'] or 'localhost'}:3016"

View file

@ -13,17 +13,15 @@ services:
- ./npm-shrinkwrap.json:/app/npm-shrinkwrap.json
- node_modules:/app/node_modules
- ./bin:/app/bin
# Copying the whole public dir is fine for now, and needed for
# some unit tests to pass, but we will want to isolate the coffee
# and vendor js files, so that the compiled js files are not written
# back to the local filesystem.
- ./public:/app/public
- ./public/coffee:/app/public/coffee:ro
- ./public/js/ace-1.2.5:/app/public/js/ace-1.2.5
- ./app.coffee:/app/app.coffee:ro
- ./app/coffee:/app/app/coffee:ro
- ./app/templates:/app/app/templates:ro
- ./app/views:/app/app/views:ro
- ./config:/app/config
- ./test/unit/coffee:/app/test/unit/coffee:ro
- ./test/unit_frontend/coffee:/app/test/unit_frontend/coffee:ro
- ./test/acceptance/coffee:/app/test/acceptance/coffee:ro
- ./test/acceptance/files:/app/test/acceptance/files:ro
- ./test/smoke/coffee:/app/test/smoke/coffee:ro

View file

@ -14,11 +14,15 @@
"test:acceptance:run": "bin/acceptance_test $@",
"test:acceptance:dir": "npm -q run compile:acceptance_tests && npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@",
"test:acceptance": "npm -q run test:acceptance:dir -- $@ test/acceptance/js",
"test:unit": "npm -q run compile:app && npm -q run compile:unit_tests && bin/unit_test $@",
"test:unit": "npm -q run compile:backend && npm -q run compile:unit_tests && bin/unit_test $@",
"test:frontend": "npm -q run compile:frontend && npm -q run compile:frontend_tests && bin/frontend_test $@",
"compile:unit_tests": "bin/compile_unit_tests",
"compile:frontend_tests": "bin/compile_frontend_tests",
"compile:acceptance_tests": "bin/compile_acceptance_tests",
"compile:app": "bin/compile_app",
"start": "npm -q run compile:app && node app.js"
"compile:frontend": "bin/compile_frontend",
"compile:backend": "bin/compile_backend",
"compile": "npm -q run compile:backend && npm -q run compile:frontend",
"start": "npm -q run compile && node app.js"
},
"dependencies": {
"archiver": "0.9.0",

View file

@ -1,5 +1,5 @@
define [
"libs"
"libraries"
"modules/recursionHelper"
"modules/errorCatcher"
"modules/localStorage"

View file

@ -49,18 +49,21 @@ define [
selectAllListController.clearSelectAllState()
scope.$on "select-all:select", () ->
return if element.prop('disabled')
ignoreChanges = true
scope.$apply () ->
scope.ngModel = true
ignoreChanges = false
scope.$on "select-all:deselect", () ->
return if element.prop('disabled')
ignoreChanges = true
scope.$apply () ->
scope.ngModel = false
ignoreChanges = false
scope.$on "select-all:row-clicked", () ->
return if element.prop('disabled')
ignoreChanges = true
scope.$apply () ->
scope.ngModel = !scope.ngModel
@ -75,4 +78,4 @@ define [
link: (scope, element, attrs) ->
element.on "click", (e) ->
scope.$broadcast "select-all:row-clicked"
}
}

View file

@ -5,6 +5,7 @@ define [
"ide/editor/EditorManager"
"ide/online-users/OnlineUsersManager"
"ide/history/HistoryManager"
"ide/history/HistoryV2Manager"
"ide/permissions/PermissionsManager"
"ide/pdf/PdfManager"
"ide/binary-files/BinaryFilesManager"
@ -44,6 +45,7 @@ define [
EditorManager
OnlineUsersManager
HistoryManager
HistoryV2Manager
PermissionsManager
PdfManager
BinaryFilesManager
@ -137,7 +139,10 @@ define [
ide.fileTreeManager = new FileTreeManager(ide, $scope)
ide.editorManager = new EditorManager(ide, $scope)
ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
ide.historyManager = new HistoryManager(ide, $scope)
if window.data.useV2History
ide.historyManager = new HistoryV2Manager(ide, $scope)
else
ide.historyManager = new HistoryManager(ide, $scope)
ide.pdfManager = new PdfManager(ide, $scope)
ide.permissionsManager = new PermissionsManager(ide, $scope)
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)

View file

@ -3,9 +3,22 @@ define [
"ide/colors/ColorManager"
], (App, ColorManager) ->
App.controller "ChatMessageController", ["$scope", "ide", ($scope, ide) ->
$scope.hue = (user) ->
hslColorConfigs =
borderSaturation: window.uiConfig?.chatMessageBorderSaturation or "70%"
borderLightness : window.uiConfig?.chatMessageBorderLightness or "70%"
bgSaturation : window.uiConfig?.chatMessageBgSaturation or "60%"
bgLightness : window.uiConfig?.chatMessageBgLightness or "97%"
hue = (user) ->
if !user?
return 0
else
return ColorManager.getHueForUserId(user.id)
$scope.getMessageStyle = (user) ->
"border-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
"background-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.bgSaturation }, #{ hslColorConfigs.bgLightness })"
$scope.getArrowStyle = (user) ->
"border-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
]

View file

@ -33,7 +33,7 @@ define [
if !ace.config._moduleUrl?
ace.config._moduleUrl = ace.config.moduleUrl
ace.config.moduleUrl = (args...) ->
url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}"
url = ace.config._moduleUrl(args...)
return url
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, metadata, graphics, preamble, files, $http, $q) ->

View file

@ -100,6 +100,7 @@ define [
end_ts: end_ts
doc: doc
error: false
pathname: doc.name
}
if !doc.deleted
@ -190,8 +191,10 @@ define [
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
for update in updates
update.pathnames = [] # Used for display
for doc_id, doc of update.docs or {}
doc.entity = @ide.fileTreeManager.findEntityById(doc_id, includeDeleted: true)
update.pathnames.push doc.entity.name
for user in update.meta.users or []
if user?

View file

@ -0,0 +1,280 @@
define [
"moment"
"ide/colors/ColorManager"
"ide/history/controllers/HistoryListController"
"ide/history/controllers/HistoryDiffController"
"ide/history/directives/infiniteScroll"
], (moment, ColorManager) ->
class HistoryManager
constructor: (@ide, @$scope) ->
@reset()
@$scope.toggleHistory = () =>
if @$scope.ui.view == "history"
@hide()
else
@show()
@$scope.$watch "history.selection.updates", (updates) =>
if updates? and updates.length > 0
@_selectDocFromUpdates()
@reloadDiff()
@$scope.$on "entity:selected", (event, entity) =>
if (@$scope.ui.view == "history") and (entity.type == "doc")
@$scope.history.selection.pathname = _ide.fileTreeManager.getEntityPath(entity)
@reloadDiff()
show: () ->
@$scope.ui.view = "history"
@reset()
hide: () ->
@$scope.ui.view = "editor"
# Make sure we run the 'open' logic for whatever is currently selected
@$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity()
reset: () ->
@$scope.history = {
isV2: true
updates: []
nextBeforeTimestamp: null
atEnd: false
selection: {
updates: []
pathname: null
range: {
fromV: null
toV: null
}
}
diff: null
}
MAX_RECENT_UPDATES_TO_SELECT: 2
autoSelectRecentUpdates: () ->
return if @$scope.history.updates.length == 0
@$scope.history.updates[0].selectedTo = true
indexOfLastUpdateNotByMe = 0
for update, i in @$scope.history.updates
if @_updateContainsUserId(update, @$scope.user.id) or i > @MAX_RECENT_UPDATES_TO_SELECT
break
indexOfLastUpdateNotByMe = i
@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
BATCH_SIZE: 10
fetchNextBatchOfUpdates: () ->
url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
if @$scope.history.nextBeforeTimestamp?
url += "&before=#{@$scope.history.nextBeforeTimestamp}"
@$scope.history.loading = true
@ide.$http
.get(url)
.then (response) =>
{ data } = response
@_loadUpdates(data.updates)
@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
if !data.nextBeforeTimestamp?
@$scope.history.atEnd = true
@$scope.history.loading = false
reloadDiff: () ->
diff = @$scope.history.diff
{updates} = @$scope.history.selection
{fromV, toV, pathname} = @_calculateDiffDataFromSelection()
if !pathname?
@$scope.history.diff = null
return
return if diff? and
diff.pathname == pathname and
diff.fromV == fromV and
diff.toV == toV
@$scope.history.diff = diff = {
fromV: fromV
toV: toV
pathname: pathname
error: false
}
diff.loading = true
url = "/project/#{@$scope.project_id}/diff"
query = ["pathname=#{encodeURIComponent(pathname)}"]
if diff.fromV? and diff.toV?
query.push "from=#{diff.fromV}", "to=#{diff.toV}"
url += "?" + query.join("&")
@ide.$http
.get(url)
.then (response) =>
{ data } = response
diff.loading = false
{text, highlights} = @_parseDiff(data)
diff.text = text
diff.highlights = highlights
.catch () ->
diff.loading = false
diff.error = true
_parseDiff: (diff) ->
row = 0
column = 0
highlights = []
text = ""
for entry, i in diff.diff or []
content = entry.u or entry.i or entry.d
content ||= ""
text += content
lines = content.split("\n")
startRow = row
startColumn = column
if lines.length > 1
endRow = startRow + lines.length - 1
endColumn = lines[lines.length - 1].length
else
endRow = startRow
endColumn = startColumn + lines[0].length
row = endRow
column = endColumn
range = {
start:
row: startRow
column: startColumn
end:
row: endRow
column: endColumn
}
if entry.i? or entry.d?
if entry.meta.user?
name = "#{entry.meta.user.first_name} #{entry.meta.user.last_name}"
else
name = "Anonymous"
if entry.meta.user?.id == @$scope.user.id
name = "you"
date = moment(entry.meta.end_ts).format("Do MMM YYYY, h:mm a")
if entry.i?
highlights.push {
label: "Added by #{name} on #{date}"
highlight: range
hue: ColorManager.getHueForUserId(entry.meta.user?.id)
}
else if entry.d?
highlights.push {
label: "Deleted by #{name} on #{date}"
strikeThrough: range
hue: ColorManager.getHueForUserId(entry.meta.user?.id)
}
return {text, highlights}
_loadUpdates: (updates = []) ->
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
for update in updates or []
for user in update.meta.users or []
if user?
user.hue = ColorManager.getHueForUserId(user.id)
if !previousUpdate? or !moment(previousUpdate.meta.end_ts).isSame(update.meta.end_ts, "day")
update.meta.first_in_day = true
update.selectedFrom = false
update.selectedTo = false
update.inSelection = false
previousUpdate = update
firstLoad = @$scope.history.updates.length == 0
@$scope.history.updates =
@$scope.history.updates.concat(updates)
@autoSelectRecentUpdates() if firstLoad
_perDocSummaryOfUpdates: (updates) ->
# Track current_pathname -> original_pathname
original_pathnames = {}
# Map of original pathname -> doc summary
docs_summary = {}
updatePathnameWithUpdateVersions = (pathname, update) ->
# docs_summary is indexed by the original pathname the doc
# had at the start, so we have to look this up from the current
# pathname via original_pathname first
if !original_pathnames[pathname]?
original_pathnames[pathname] = pathname
original_pathname = original_pathnames[pathname]
doc_summary = docs_summary[original_pathname] ?= {
fromV: update.fromV, toV: update.toV,
}
doc_summary.fromV = Math.min(
doc_summary.fromV,
update.fromV
)
doc_summary.toV = Math.max(
doc_summary.toV,
update.toV
)
# Put updates in ascending chronological order
updates = updates.slice().reverse()
for update in updates
for pathname in update.pathnames or []
updatePathnameWithUpdateVersions(pathname, update)
for project_op in update.project_ops or []
if project_op.rename?
rename = project_op.rename
updatePathnameWithUpdateVersions(rename.pathname, update)
original_pathnames[rename.newPathname] = original_pathnames[rename.pathname]
delete original_pathnames[rename.pathname]
if project_op.add?
add = project_op.add
updatePathnameWithUpdateVersions(add.pathname, update)
return docs_summary
_calculateDiffDataFromSelection: () ->
fromV = toV = pathname = null
selected_pathname = @$scope.history.selection.pathname
for pathname, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
if pathname == selected_pathname
{fromV, toV} = doc
return {fromV, toV, pathname}
return {}
# Set the track changes selected doc to one of the docs in the range
# of currently selected updates. If we already have a selected doc
# then prefer this one if present.
_selectDocFromUpdates: () ->
affected_docs = @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
selected_pathname = @$scope.history.selection.pathname
if selected_pathname? and affected_docs[selected_pathname]
# Selected doc is already open
else
# Set to first possible candidate
for pathname, doc of affected_docs
selected_pathname = pathname
break
@$scope.history.selection.pathname = selected_pathname
if selected_pathname?
entity = @ide.fileTreeManager.findEntityByPath(selected_pathname)
if entity?
@ide.fileTreeManager.selectEntity(entity)
_updateContainsUserId: (update, user_id) ->
for user in update.meta.users
return true if user?.id == user_id
return false

View file

@ -90,7 +90,9 @@ define [
# to block auto compiles. It also causes problems where server-provided
# linting errors aren't cleared after typing
if (ide.$scope.settings.syntaxValidation and !ide.$scope.hasLintingError)
$scope.recompile(isAutoCompileOnChange: true)
$scope.recompile(isAutoCompileOnChange: true) # compile if no linting errors
else if !ide.$scope.settings.syntaxValidation
$scope.recompile(isAutoCompileOnChange: true) # always recompile
else
# Extend remainder of timeout
autoCompileTimeout = setTimeout () ->
@ -533,14 +535,6 @@ define [
else
$scope.switchToSideBySideLayout()
$scope.startFreeTrial = (source) ->
ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source)
event_tracking.sendMB "subscription-start-trial", { source }
window.open("/user/subscription/new?planCode=#{$scope.startTrialPlanCode}")
$scope.startedFreeTrial = true
App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
# enable per-user containers by default
perUserCompile = true

View file

@ -4,8 +4,3 @@ define [
App.controller "TrackChangesUpgradeModalController", ($scope, $modalInstance) ->
$scope.cancel = () ->
$modalInstance.dismiss()
$scope.startFreeTrial = (source) ->
ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
window.open("/user/subscription/new?planCode=student_free_trial_7_days")
$scope.startedFreeTrial = true

View file

@ -9,7 +9,6 @@ define [
"libs/angular-cookie"
"libs/passfield"
"libs/sixpack"
"libs/groove"
"libs/angular-sixpack"
"libs/ng-tags-input-3.0.0"
], () ->

View file

@ -11,9 +11,12 @@ define [
w = window.open()
go = () ->
ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
url = "/user/subscription/new?planCode=#{plan}&ssp=true"
if couponCode?
url = "#{url}&cc=#{couponCode}"
if window.redirectToOLFreeTrialUrl?
url = window.redirectToOLFreeTrialUrl
else
url = "/user/subscription/new?planCode=#{plan}&ssp=true"
if couponCode?
url = "#{url}&cc=#{couponCode}"
$scope.startedFreeTrial = true
switch source
@ -27,7 +30,7 @@ define [
else
event_tracking.sendMB "subscription-start-trial", { source, plan }
w.location = url
if $scope.shouldABTestPlans

View file

@ -74,27 +74,34 @@ define [
$modalInstance.close()
App.controller 'UniverstiesContactController', ($scope, $modal) ->
App.controller 'UniverstiesContactController', ($scope, $modal, $http) ->
$scope.form = {}
$scope.sent = false
$scope.sending = false
$scope.error = false
$scope.contactUs = ->
if !$scope.form.email?
console.log "email not set"
return
$scope.sending = true
ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32)
params =
data =
_csrf : window.csrfToken
name: $scope.form.name || $scope.form.email
email: $scope.form.email
labels: "#{$scope.form.source} accounts"
message: "Please contact me with more details"
subject: $scope.form.subject + " - [#{ticketNumber}]"
about : "#{$scope.form.position || ''} #{$scope.form.university || ''}"
subject: "#{$scope.form.name} - General Enquiry - #{$scope.form.position} - #{$scope.form.university}"
inbox: "accounts"
Groove.createTicket params, (err, json)->
$scope.sent = true
request = $http.post "/support", data
request.catch ()->
$scope.error = true
$scope.$apply()
request.then (response)->
$scope.sent = true
$scope.error = (response.status != 200)
$scope.$apply()

View file

@ -462,6 +462,9 @@ define [
App.controller "ProjectListItemController", ($scope) ->
$scope.shouldDisableCheckbox = (project) ->
$scope.filter == 'archived' && project.accessLevel != 'owner'
$scope.projectLink = (project) ->
if project.accessLevel == 'readAndWrite' and project.source == 'token'
"/#{project.tokens.readAndWrite}"

View file

@ -1,5 +1,5 @@
define [
"libs"
"libraries"
], () ->
angular.module('underscore', []).factory '_', ->
return window._

View file

@ -1,84 +0,0 @@
!function(window) {
window.Groove = {
init: function(options) {
this._options = options;
if (typeof grooveOnReady != 'undefined') {grooveOnReady();}
},
createTicket: function(params, callback) {
var postData = serialize({
"ticket[enduser_name]": params["name"],
"ticket[enduser_email]": params["email"],
"ticket[title]": params["subject"],
"ticket[enduser_about]": params["about"],
"ticket[label_string]": params["labels"],
"ticket[comments_attributes][0][body]": params["message"]
});
sendRequest(this._options.widget_ticket_url, function(req) {
if (callback) {callback(req);}
}, postData);
}
};
// http://www.quirksmode.org/js/xmlhttp.html
function sendRequest(url, callback, postData) {
var req = createXMLHTTPObject();
if (!req) return;
var method = (postData) ? "POST" : "GET";
req.open(method, url, true);
if (postData){
try {
req.setRequestHeader('Content-type','application/x-www-form-urlencoded');
}
catch(e) {
req.contentType = 'application/x-www-form-urlencoded';
};
};
req.onreadystatechange = function () {
if (req.readyState != 4) return;
callback(req);
}
if (req.readyState == 4) return;
req.send(postData);
}
var XMLHttpFactories = [
function () {return new XDomainRequest()},
function () {return new XMLHttpRequest()},
function () {return new ActiveXObject("Msxml2.XMLHTTP")},
function () {return new ActiveXObject("Msxml3.XMLHTTP")},
function () {return new ActiveXObject("Microsoft.XMLHTTP")}
];
function createXMLHTTPObject() {
var xmlhttp = false;
for (var i = 0; i < XMLHttpFactories.length; i++) {
try {
xmlhttp = XMLHttpFactories[i]();
}
catch (e) {
continue;
}
break;
}
return xmlhttp;
}
function serialize(obj) {
var str = [];
for(var p in obj) {
if (obj[p]) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
}
return str.join("&");
}
if (typeof grooveOnLoad != 'undefined') {grooveOnLoad();}
}(window);
Groove.init({"widget_ticket_url":"https://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket.json"});

View file

@ -0,0 +1 @@
@import "app/sidebar-v2-dash-pane.less";

View file

@ -31,13 +31,18 @@
right: 0;
bottom: @new-message-height;
overflow-x: hidden;
background-color: @chat-bg;
li.message {
margin: @line-height-computed / 2;
.date {
font-size: 12px;
color: @gray-light;
border-bottom: 1px solid @gray-lightest;
color: @chat-message-date-color;
margin-bottom: @line-height-computed / 2;
text-align: right;
}
.date when (@is-overleaf = false) {
border-bottom: 1px solid @gray-lightest;
text-align: center;
}
.avatar {
@ -56,20 +61,22 @@
.name {
font-size: 12px;
color: @gray-light;
color: @chat-message-name-color;
margin-bottom: 4px;
min-height: 16px;
}
.message {
border-left: 3px solid transparent;
font-size: 14px;
box-shadow: -1px 2px 3px #ddd;
border-raduis: @border-radius-base;
box-shadow: @chat-message-box-shadow;
border-radius: @chat-message-border-radius;
position: relative;
.message-content {
padding: @line-height-computed / 2;
padding: @chat-message-padding;
overflow-x: auto;
color: @chat-message-color;
font-weight: @chat-message-weight;
}
.arrow {
@ -124,7 +131,7 @@
.full-size;
top: auto;
height: @new-message-height;
background-color: @gray-lightest;
background-color: @chat-new-message-bg;
padding: @line-height-computed / 4;
border-top: 1px solid @editor-border-color;
textarea {
@ -134,9 +141,10 @@
border: 1px solid @editor-border-color;
height: 100%;
width: 100%;
color: @gray-dark;
color: @chat-new-message-textarea-color;
font-size: 14px;
padding: @line-height-computed / 4;
background-color: @chat-new-message-textarea-bg;
}
}
}
@ -145,7 +153,7 @@
word-break: break-all;
}
.editor-dark {
.editor-dark when (@is-overleaf = false) {
.chat {
.new-message {
background-color: lighten(@editor-dark-background-color, 10%);

View file

@ -169,9 +169,19 @@
font-size: 0.8rem;
line-height: @line-height-computed;
}
.docs {
font-weight: bold;
.doc {
font-size: 0.9rem;
font-weight: bold;
}
.action {
color: @gray;
text-transform: uppercase;
font-size: 0.7em;
margin-bottom: -2px;
margin-top: 2px;
&-edited {
margin-top: 0;
}
}
}
li.loading-changes, li.empty-message {

View file

@ -10,9 +10,13 @@
padding: 0 (@line-height-computed / 2);
}
.pdf {
background-color: @pdf-bg;
}
.pdf-viewer, .pdf-logs, .pdf-errors, .pdf-uncompiled {
.full-size;
top: 58px;
top: @pdf-top-offset;
}
.pdf-logs, .pdf-errors, .pdf-uncompiled, .pdf-validation-problems{
@ -69,11 +73,11 @@
}
.pdfjs-viewer {
.full-size;
background-color: @gray-lighter;
background-color: @pdfjs-bg;
overflow: scroll;
canvas, div.pdf-canvas {
background: white;
box-shadow: black 0px 0px 10px;
box-shadow: @pdf-page-shadow-color 0px 0px 10px;
}
div.pdf-canvas.pdfng-empty {
background-color: white;
@ -179,7 +183,7 @@
cursor: pointer;
.line-no {
float: right;
color: @gray;
color: @log-line-no-color;
font-weight: 700;
.fa {
@ -203,16 +207,25 @@
}
}
&.alert-danger:hover {
background-color: darken(@alert-danger-bg, 5%);
&.alert-danger {
background-color: tint(@alert-danger-bg, 15%);
&:hover {
background-color: @alert-danger-bg;
}
}
&.alert-warning:hover {
background-color: darken(@alert-warning-bg, 5%);
&.alert-warning {
background-color: tint(@alert-warning-bg, 15%);
&:hover {
background-color: @alert-warning-bg;
}
}
&.alert-info:hover {
background-color: darken(@alert-info-bg, 5%);
&.alert-info {
background-color: tint(@alert-info-bg, 15%);
&:hover {
background-color: @alert-info-bg;
}
}
}
@ -354,22 +367,22 @@
}
.alert-danger & {
color: @alert-danger-border;
color: @state-danger-border;
}
.alert-warning & {
color: @alert-warning-border;
color: @state-warning-border;
}
.alert-info & {
color: @alert-info-border;
color: @state-info-border;
}
}
&-text,
&-feedback-label {
color: @gray-dark;
color: @log-hints-color;
font-size: 0.9rem;
margin-bottom: 20px;
}
@ -394,25 +407,25 @@
&-actions a,
&-text a {
.alert-danger & {
color: @alert-danger-text;
color: @state-danger-text;
}
.alert-warning & {
color: @alert-warning-text;
color: @state-warning-text;
}
.alert-info & {
color: @alert-info-text;
color: @state-info-text;
}
}
&-feedback {
color: @gray-dark;
color: @log-hints-color;
float: right;
}
&-extra-feedback {
color: @gray-dark;
color: @log-hints-color;
font-size: 0.8rem;
margin-top: 10px;
padding-bottom: 5px;

View file

@ -129,11 +129,11 @@
}
.toolbar-small-mixin() {
height: 32px;
height: @toolbar-small-height;
}
.toolbar-tall-mixin() {
height: 58px;
height: @toolbar-tall-height;
padding-top: 10px;
}
.toolbar-alt-mixin() {
@ -143,7 +143,7 @@
.toolbar-label {
display: none;
margin: 0 4px;
font-size: 12px;
font-size: @toolbar-font-size;
font-weight: 600;
margin-bottom: 2px;
vertical-align: middle;

View file

@ -2,8 +2,34 @@
text-align: center;
}
.v1-import-row {
display: flex;
align-items: center;
}
.v1-import-col {
flex-basis: 50%;
flex-grow: 0;
flex-shrink: 0;
padding-left: 15px;
padding-right: 15px;
}
.v1-import-col ul {
margin-bottom: 0;
}
.v1-import-img {
width: 100%;
margin-top: 30px;
}
.v1-import-cta {
margin-top: 20px;
margin-left: auto;
margin-right: auto;
width: 90%;
text-align: center;
}
.v1-import-warning {

View file

@ -57,7 +57,6 @@
}
.project-list-sidebar when (@is-overleaf) {
height: 100%;
overflow-x: hidden;
overflow-y: auto;
-ms-overflow-style: -ms-autohiding-scrollbar;

View file

@ -0,0 +1,27 @@
@v2-dash-pane-link-height: 130px;
.project-list-sidebar {
height: calc(~"100% -" @v2-dash-pane-link-height);
}
.project-list-sidebar-v2-pane {
position: absolute;
bottom: 0;
height: @v2-dash-pane-link-height;
background-color: @v2-dash-pane-bg;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
color: white;
font-size: 13px;
}
.project-list-sidebar-v2-pane a {
color: @v2-dash-pane-link-color;
text-decoration: underline;
}
.project-list-sidebar-v2-pane a:hover {
text-decoration: none;
}

View file

@ -10,7 +10,7 @@
padding: @alert-padding;
margin-bottom: @line-height-computed;
border-left: 3px solid transparent;
// border-radius: @alert-border-radius;
border-radius: @alert-border-radius;
// Headings for larger alerts
h4 {

View file

@ -1,7 +1,6 @@
.card {
background-color: white;
border-radius: @border-radius-base;
-webkit-box-shadow: @card-box-shadow;
box-shadow: @card-box-shadow;
padding: @line-height-computed;
.page-header {

View file

@ -20,14 +20,9 @@
//== Typography
//
//## Font, line-height, and color for body text, headings, and more.
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,600,700);
//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i);
@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
@font-family-sans-serif: "Open Sans", sans-serif;
@font-family-serif: "Merriweather", serif;
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
@font-family-base: @font-family-sans-serif;
@ -550,7 +545,7 @@
//## Define alert colors, border radius, and padding.
@alert-padding: 15px;
@alert-border-radius: @border-radius-base;
@alert-border-radius: 0;
@alert-link-font-weight: bold;
@alert-success-bg: @state-success-bg;
@ -898,25 +893,28 @@
@toolbar-btn-active-color : white;
@toolbar-btn-active-bg-color : @link-color;
@toolbar-btn-active-shadow : inset 0 3px 5px rgba(0, 0, 0, 0.225);
@toolbar-font-size : 12px;
@toolbar-alt-bg-color : #fafafa;
@toolbar-icon-btn-color : @gray-light;
@toolbar-icon-btn-hover-color : @gray-dark;
@toolbar-icon-btn-hover-shadow : 0 1px 0 rgba(0, 0, 0, 0.25);
@toolbar-icon-btn-hover-boxshadow : inset 0 3px 5px rgba(0, 0, 0, 0.225);
@toolbar-border-bottom : 1px solid @toolbar-border-color;
@toolbar-border-bottom : 1px solid @toolbar-border-color;
@toolbar-small-height : 32px;
@toolbar-tall-height : 58px;
// Editor file-tree
@file-tree-bg : transparent;
@file-tree-line-height : 2.6;
@file-tree-item-color : @gray-darker;
@file-tree-item-toggle-color : @gray;
@file-tree-item-icon-color : @gray-light;
@file-tree-item-input-color : inherit;
@file-tree-item-folder-color : lighten(desaturate(@link-color, 10%), 5%);
@file-tree-item-hover-bg : @gray-lightest;
@file-tree-item-selected-bg : transparent;
@file-tree-multiselect-bg : lighten(@brand-info, 40%);
@file-tree-multiselect-hover-bg : lighten(@brand-info, 30%);
@file-tree-bg : transparent;
@file-tree-line-height : 2.6;
@file-tree-item-color : @gray-darker;
@file-tree-item-toggle-color : @gray;
@file-tree-item-icon-color : @gray-light;
@file-tree-item-input-color : inherit;
@file-tree-item-folder-color : lighten(desaturate(@link-color, 10%), 5%);
@file-tree-item-hover-bg : @gray-lightest;
@file-tree-item-selected-bg : transparent;
@file-tree-multiselect-bg : lighten(@brand-info, 40%);
@file-tree-multiselect-hover-bg : lighten(@brand-info, 30%);
// Editor resizers
@editor-resizer-bg-color : #F4F4F4;
@ -925,6 +923,28 @@
@editor-toggler-hover-bg-color : #DDD;
@synctex-controls-z-index : 3;
@synctex-controls-padding : 0 2px;
// Chat
@chat-bg : transparent;
@chat-message-color : @text-color;
@chat-message-date-color : @gray-light;
@chat-message-name-color : @gray-light;
@chat-message-box-shadow : -1px 2px 3px #ddd;
@chat-message-border-radius : 0;
@chat-message-padding : @line-height-computed / 2;
@chat-message-weight : normal;
@chat-new-message-bg : @gray-lightest;
@chat-new-message-textarea-bg : #FFF;
@chat-new-message-textarea-color : @gray-dark;
// PDF
@pdf-top-offset : @toolbar-tall-height;
@pdf-bg : transparent;
@pdfjs-bg : @gray-lighter;
@pdf-page-shadow-color : #000;
@log-line-no-color : @gray;
@log-hints-color : @gray-dark;
// Tags
@tag-border-radius : 0.25em;
@tag-bg-color : @label-default-bg;

View file

@ -1,6 +1,9 @@
@import "./_common-variables.less";
@is-overleaf: true;
@font-family-sans-serif: "Lato", sans-serif;
@header-height: 68px;
@footer-height: 50px;
@ -64,6 +67,28 @@
@btn-info-bg : @ol-blue;
@btn-info-border : transparent;
// Alerts
@alert-padding : 15px;
@alert-border-radius : @border-radius-base;
@alert-link-font-weight : bold;
@alert-success-bg : @brand-success;
@alert-success-text : #FFF;
@alert-success-border: transparent;
@alert-info-bg : @brand-info;
@alert-info-text : #FFF;
@alert-info-border : transparent;
@alert-warning-bg : @brand-warning;
@alert-warning-text : #FFF;
@alert-warning-border: transparent;
@alert-danger-bg : @brand-danger;
@alert-danger-text : #FFF;
@alert-danger-border : transparent;
// Tags
@tag-border-radius : 9999px;
@tag-bg-color : @ol-green;
@ -110,6 +135,10 @@
@sidebar-active-bg : @ol-blue-gray-6;
@sidebar-hover-bg : @ol-blue-gray-4;
@sidebar-hover-text-decoration : none;
@v2-dash-pane-bg : @ol-blue-gray-4;
@v2-dash-pane-link-color : #FFF;
@v2-dash-pane-btn-bg : @ol-blue-gray-5;
@v2-dash-pane-btn-hover-bg : @ol-blue-gray-6;
@folders-menu-margin : 0 -(@grid-gutter-width / 2);
@folders-menu-line-height : @structured-list-line-height;
@ -173,19 +202,22 @@
@toolbar-icon-btn-hover-shadow : none;
@toolbar-border-bottom : 1px solid @toolbar-border-color;
@toolbar-icon-btn-hover-boxshadow : none;
@toolbar-font-size : 13px;
// Editor file-tree
@file-tree-bg : @ol-blue-gray-4;
@file-tree-line-height : 2.05;
@file-tree-item-color : #FFF;
@file-tree-item-input-color : @ol-blue-gray-5;
@file-tree-item-toggle-color : @ol-blue-gray-2;
@file-tree-item-icon-color : @ol-blue-gray-2;
@file-tree-item-folder-color : @ol-blue-gray-2;
@file-tree-item-hover-bg : @ol-blue-gray-5;
@file-tree-item-selected-bg : @ol-green;
@file-tree-multiselect-bg : @ol-blue;
@file-tree-multiselect-hover-bg : @ol-dark-blue;
@file-tree-droppable-bg-color : tint(@ol-green, 5%);
@file-tree-bg : @ol-blue-gray-4;
@file-tree-line-height : 2.05;
@file-tree-item-color : #FFF;
@file-tree-item-input-color : @ol-blue-gray-5;
@file-tree-item-toggle-color : @ol-blue-gray-2;
@file-tree-item-icon-color : @ol-blue-gray-2;
@file-tree-item-folder-color : @ol-blue-gray-2;
@file-tree-item-hover-bg : @ol-blue-gray-5;
@file-tree-item-selected-bg : @ol-green;
@file-tree-multiselect-bg : @ol-blue;
@file-tree-multiselect-hover-bg : @ol-dark-blue;
@file-tree-droppable-bg-color : tint(@ol-green, 5%);
// Editor resizers
@editor-resizer-bg-color : @ol-blue-gray-6;
@editor-resizer-bg-color-dragging : transparent;
@ -193,6 +225,31 @@
@editor-toggler-hover-bg-color : @ol-green;
@synctex-controls-z-index : 6;
@synctex-controls-padding : 0;
@editor-border-color : @ol-blue-gray-5;
// Chat
@chat-bg : @ol-blue-gray-5;
@chat-message-color : #FFF;
@chat-message-name-color : #FFF;
@chat-message-date-color : @ol-blue-gray-2;
@chat-message-box-shadow : none;
@chat-message-padding : 5px 10px;
@chat-message-border-radius : @border-radius-large;
@chat-message-weight : bold;
@chat-new-message-bg : @ol-blue-gray-4;
@chat-new-message-textarea-bg : @ol-blue-gray-1;
@chat-new-message-textarea-color : @ol-blue-gray-6;
// PDF
@pdf-top-offset : @toolbar-small-height;
@pdf-bg : @ol-blue-gray-1;
@pdfjs-bg : transparent;
@pdf-page-shadow-color : rgba(0, 0, 0, 0.5);
@log-line-no-color : #FFF;
@log-hints-color : @ol-blue-gray-4;
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
@gray-darker: #252525;
@gray-dark: #505050;
@ -212,9 +269,9 @@
@brand-primary: @ol-green;
@brand-success: @green;
@brand-info: @ol-dark-green;
@brand-info: @ol-blue;
@brand-warning: @orange;
@brand-danger: #E03A06;
@brand-danger: @ol-red;
@editor-loading-logo-padding-top: 115.44%;
@editor-loading-logo-background-url: url(/img/ol-brand/overleaf-o-grey.svg);

View file

@ -1,4 +1,8 @@
@import url(https://fonts.googleapis.com/css?family=Lato:300,400,700&subset=latin-ext);
@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
// Core variables and mixins
@import "core/ol-variables.less";
@import "app/ol-style-guide.less";
@import "_style_includes.less";
@import "_style_includes.less";
@import "_ol_style_includes.less";

View file

@ -1,3 +1,6 @@
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
// Core variables and mixins
@import "core/variables.less";
@import "_style_includes.less";

View file

@ -59,7 +59,7 @@ describe "ProjectStructureChanges", ->
@dup_project_id = body.project_id
done()
it "should version the dosc created", ->
it "should version the docs created", ->
updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).docUpdates
expect(updates.length).to.equal(2)
_.each updates, (update) =>
@ -91,6 +91,7 @@ describe "ProjectStructureChanges", ->
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to add doc #{res.statusCode}")
@example_doc_id = body._id
done()
it "should version the doc added", ->
@ -162,6 +163,8 @@ describe "ProjectStructureChanges", ->
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to upload file #{res.statusCode}")
@example_file_id = JSON.parse(body).entity_id
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
expect(updates.length).to.equal(1)
update = updates[0]
@ -199,6 +202,120 @@ describe "ProjectStructureChanges", ->
done()
describe "moving entities", ->
before (done) ->
@owner.request.post {
uri: "project/#{@example_project_id}/folder",
formData:
name: 'foo'
}, (error, res, body) =>
throw error if error?
@example_folder_id_1 = JSON.parse(body)._id
done()
beforeEach () ->
MockDocUpdaterApi.clearProjectStructureUpdates()
it "should version moving a doc", (done) ->
@owner.request.post {
uri: "project/#{@example_project_id}/Doc/#{@example_doc_id}/move",
json:
folder_id: @example_folder_id_1
}, (error, res, body) =>
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to move doc #{res.statusCode}")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/new.tex")
expect(update.newPathname).to.equal("/foo/new.tex")
done()
it "should version moving a file", (done) ->
@owner.request.post {
uri: "project/#{@example_project_id}/File/#{@example_file_id}/move",
json:
folder_id: @example_folder_id_1
}, (error, res, body) =>
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to move file #{res.statusCode}")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/1pixel.png")
expect(update.newPathname).to.equal("/foo/1pixel.png")
done()
it "should version moving a folder", (done) ->
@owner.request.post {
uri: "project/#{@example_project_id}/folder",
formData:
name: 'bar'
}, (error, res, body) =>
throw error if error?
@example_folder_id_2 = JSON.parse(body)._id
@owner.request.post {
uri: "project/#{@example_project_id}/Folder/#{@example_folder_id_1}/move",
json:
folder_id: @example_folder_id_2
}, (error, res, body) =>
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to move folder #{res.statusCode}")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/foo/new.tex")
expect(update.newPathname).to.equal("/bar/foo/new.tex")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/foo/1pixel.png")
expect(update.newPathname).to.equal("/bar/foo/1pixel.png")
done()
describe "deleting entities", ->
beforeEach () ->
MockDocUpdaterApi.clearProjectStructureUpdates()
it "should version deleting a folder", (done) ->
@owner.request.delete {
uri: "project/#{@example_project_id}/Folder/#{@example_folder_id_2}",
}, (error, res, body) =>
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to delete folder #{res.statusCode}")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/bar/foo/new.tex")
expect(update.newPathname).to.equal("")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/bar/foo/1pixel.png")
expect(update.newPathname).to.equal("")
done()
describe "tpds", ->
before (done) ->
@tpds_project_name = "tpds-project-#{new ObjectId().toString()}"
@ -305,3 +422,25 @@ describe "ProjectStructureChanges", ->
done()
image_file.pipe(req)
it "should version deleting a doc", (done) ->
req = @owner.request.delete {
uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/test.tex",
auth:
user: _.keys(Settings.httpAuthUsers)[0]
pass: _.values(Settings.httpAuthUsers)[0]
sendImmediately: true
}, (error, res, body) =>
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to delete doc #{res.statusCode}")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).docUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/test.tex")
expect(update.newPathname).to.equal("")
done()

View file

@ -33,6 +33,9 @@ module.exports = MockDocUpdaterApi =
@addProjectStructureUpdates(project_id, userId, docUpdates, fileUpdates)
res.sendStatus 200
app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
res.send 204
app.listen 3003, (error) ->
throw error if error?
.on "error", (error) ->

View file

@ -23,6 +23,16 @@ module.exports = MockDocStoreApi =
docs = (doc for doc_id, doc of @docs[req.params.project_id])
res.send JSON.stringify docs
app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
{project_id, doc_id} = req.params
if !@docs[project_id]?
res.send 404
else if !@docs[project_id][doc_id]?
res.send 404
else
@docs[project_id][doc_id] = undefined
res.send 204
app.listen 3016, (error) ->
throw error if error?
.on "error", (error) ->

View file

@ -393,7 +393,7 @@ describe 'DocumentUpdaterHandler', ->
describe "with project history disabled", ->
beforeEach ->
@settings.apis.project_history.enabled = false
@settings.apis.project_history.sendProjectStructureOps = false
@request.post = sinon.stub()
@handler.updateProjectStructure @project_id, @user_id, {}, @callback
@ -406,7 +406,7 @@ describe 'DocumentUpdaterHandler', ->
describe "with project history enabled", ->
beforeEach ->
@settings.apis.project_history.enabled = true
@settings.apis.project_history.sendProjectStructureOps = true
@url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}"
@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "")
@ -478,14 +478,22 @@ describe 'DocumentUpdaterHandler', ->
.should.equal true
done()
describe "when a doc has been deleted", ->
it 'should do nothing', (done) ->
describe "when an entity has been deleted", ->
it 'should end the structure update to the document updater', (done) ->
@docId = new ObjectId()
@changes = oldDocs: [
{ path: '/foo', docLines: 'a\nb', doc: _id: @docId }
]
docUpdates = [
id: @docId.toString(),
pathname: '/foo',
newPathname: ''
]
@handler.updateProjectStructure @project_id, @user_id, @changes, () =>
@request.post.called.should.equal false
@request.post
.calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id})
.should.equal true
done()

View file

@ -376,58 +376,53 @@ describe "EditorController", ->
err.should.equal "timed out"
done()
describe "deleteEntity", ->
beforeEach ->
@LockManager.getLock.callsArgWith(1)
@LockManager.releaseLock.callsArgWith(1)
@EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(4)
@EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(5)
it "should call deleteEntityWithoutLock", (done)->
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, =>
@EditorController.deleteEntityWithoutLock.calledWith(@project_id, @entity_id, @type, @source).should.equal true
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, =>
@EditorController.deleteEntityWithoutLock
.calledWith(@project_id, @entity_id, @type, @source, @user_id)
.should.equal true
done()
it "should take the lock", (done)->
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, =>
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, =>
@LockManager.getLock.calledWith(@project_id).should.equal true
done()
it "should release the lock", (done)->
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, (error)=>
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, (error) =>
@LockManager.releaseLock.calledWith(@project_id).should.equal true
done()
it "should error if it can't cat the lock", (done)->
@LockManager.getLock = sinon.stub().callsArgWith(1, "timed out")
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, (err)=>
expect(err).to.exist
err.should.equal "timed out"
done()
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, (error) =>
expect(error).to.exist
error.should.equal "timed out"
done()
describe 'deleteEntityWithoutLock', ->
beforeEach ->
@ProjectEntityHandler.deleteEntity = (project_id, entity_id, type, callback)-> callback()
beforeEach (done) ->
@entity_id = "entity_id_here"
@type = "doc"
@EditorRealTimeController.emitToRoom = sinon.stub()
@ProjectEntityHandler.deleteEntity = sinon.stub().callsArg(4)
@EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, @user_id, done
it 'should delete the folder using the project entity handler', (done)->
mock = sinon.mock(@ProjectEntityHandler).expects("deleteEntity").withArgs(@project_id, @entity_id, @type).callsArg(3)
it 'should delete the folder using the project entity handler', ->
@ProjectEntityHandler.deleteEntity
.calledWith(@project_id, @entity_id, @type, @user_id)
.should.equal.true
@EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, ->
mock.verify()
done()
it 'notify users an entity has been deleted', (done)->
@EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, =>
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, "removeEntity", @entity_id, @source)
.should.equal true
done()
it 'notify users an entity has been deleted', ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, "removeEntity", @entity_id, @source)
.should.equal true
describe "getting a list of project paths", ->

View file

@ -331,12 +331,12 @@ describe "EditorHttpController", ->
Project_id: @project_id
entity_id: @entity_id = "entity-id-123"
entity_type: @entity_type = "entity-type"
@EditorController.deleteEntity = sinon.stub().callsArg(4)
@EditorController.deleteEntity = sinon.stub().callsArg(5)
@EditorHttpController.deleteEntity @req, @res
it "should call EditorController.deleteEntity", ->
@EditorController.deleteEntity
.calledWith(@project_id, @entity_id, @entity_type, "editor")
.calledWith(@project_id, @entity_id, @entity_type, "editor", @userId)
.should.equal true
it "should send back a success response", ->

View file

@ -31,7 +31,7 @@ describe "HistoryController", ->
describe "for a project with project history", ->
beforeEach ->
@ProjectDetailsHandler.getDetails = sinon.stub().callsArgWith(1, null, {overleaf:{history:{display:true}}})
@ProjectDetailsHandler.getDetails = sinon.stub().callsArgWith(1, null, {overleaf:{history:{id: 42, display:true}}})
@HistoryController.selectHistoryApi @req, @res, @next
it "should set the flag for project history to true", ->
@ -57,93 +57,55 @@ describe "HistoryController", ->
on: (event, handler) -> @events[event] = handler
@request.returns @proxy
describe "with project history enabled", ->
describe "for a project with the project history flag", ->
beforeEach ->
@settings.apis.project_history.enabled = true
@req.useProjectHistory = true
@HistoryController.proxyToHistoryApi @req, @res, @next
describe "for a project with the project history flag", ->
beforeEach ->
@req.useProjectHistory = true
@HistoryController.proxyToHistoryApi @req, @res, @next
it "should get the user id", ->
@AuthenticationController.getLoggedInUserId
.calledWith(@req)
.should.equal true
it "should get the user id", ->
@AuthenticationController.getLoggedInUserId
.calledWith(@req)
.should.equal true
it "should call the project history api", ->
@request
.calledWith({
url: "#{@settings.apis.project_history.url}#{@req.url}"
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
it "should call the project history api", ->
@request
.calledWith({
url: "#{@settings.apis.project_history.url}#{@req.url}"
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
it "should pipe the response to the client", ->
@proxy.pipe
.calledWith(@res)
.should.equal true
it "should pipe the response to the client", ->
@proxy.pipe
.calledWith(@res)
.should.equal true
describe "for a project without the project history flag", ->
beforeEach ->
@req.useProjectHistory = false
@HistoryController.proxyToHistoryApi @req, @res, @next
it "should get the user id", ->
@AuthenticationController.getLoggedInUserId
.calledWith(@req)
.should.equal true
it "should call the track changes api", ->
@request
.calledWith({
url: "#{@settings.apis.trackchanges.url}#{@req.url}"
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
it "should pipe the response to the client", ->
@proxy.pipe
.calledWith(@res)
.should.equal true
describe "with project history disabled", ->
describe "for a project without the project history flag", ->
beforeEach ->
@settings.apis.project_history.enabled = false
@req.useProjectHistory = false
@HistoryController.proxyToHistoryApi @req, @res, @next
describe "for a project with the project history flag", ->
beforeEach ->
@req.useProjectHistory = true
@HistoryController.proxyToHistoryApi @req, @res, @next
it "should get the user id", ->
@AuthenticationController.getLoggedInUserId
.calledWith(@req)
.should.equal true
it "should call the track changes api", ->
@request
.calledWith({
url: "#{@settings.apis.trackchanges.url}#{@req.url}"
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
it "should call the track changes api", ->
@request
.calledWith({
url: "#{@settings.apis.trackchanges.url}#{@req.url}"
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
describe "for a project without the project history flag", ->
beforeEach ->
@req.useProjectHistory = false
@HistoryController.proxyToHistoryApi @req, @res, @next
it "should call the track changes api", ->
@request
.calledWith({
url: "#{@settings.apis.trackchanges.url}#{@req.url}"
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
it "should pipe the response to the client", ->
@proxy.pipe
.calledWith(@res)
.should.equal true
describe "with an error", ->
beforeEach ->
@ -152,68 +114,3 @@ describe "HistoryController", ->
it "should pass the error up the call chain", ->
@next.calledWith(@error).should.equal true
describe "initializeProject", ->
describe "with project history enabled", ->
beforeEach ->
@settings.apis.project_history.enabled = true
describe "project history returns a successful response", ->
beforeEach ->
@overleaf_id = 1234
@res = statusCode: 200
@body = JSON.stringify(project: id: @overleaf_id)
@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
@HistoryController.initializeProject @callback
it "should call the project history api", ->
@request.post.calledWith(
url: "#{@settings.apis.project_history.url}/project"
).should.equal true
it "should return the callback with the overleaf id", ->
@callback.calledWithExactly(null, { @overleaf_id }).should.equal true
describe "project history returns a response without the project id", ->
beforeEach ->
@res = statusCode: 200
@body = JSON.stringify(project: {})
@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
@HistoryController.initializeProject @callback
it "should return the callback with an error", ->
@callback
.calledWith(sinon.match.has("message", "project-history did not provide an id"))
.should.equal true
describe "project history returns a unsuccessful response", ->
beforeEach ->
@res = statusCode: 404
@request.post = sinon.stub().callsArgWith(1, null, @res)
@HistoryController.initializeProject @callback
it "should return the callback with an error", ->
@callback
.calledWith(sinon.match.has("message", "project-history returned a non-success status code: 404"))
.should.equal true
describe "project history errors", ->
beforeEach ->
@error = sinon.stub()
@request.post = sinon.stub().callsArgWith(1, @error)
@HistoryController.initializeProject @callback
it "should return the callback with the error", ->
@callback.calledWithExactly(@error).should.equal true
describe "with project history disabled", ->
beforeEach ->
@settings.apis.project_history.enabled = false
@HistoryController.initializeProject @callback
it "should return the callback", ->
@callback.calledWithExactly().should.equal true

View file

@ -0,0 +1,86 @@
chai = require('chai')
chai.should()
sinon = require("sinon")
modulePath = "../../../../app/js/Features/History/HistoryManager"
SandboxedModule = require('sandboxed-module')
describe "HistoryManager", ->
beforeEach ->
@callback = sinon.stub()
@user_id = "user-id-123"
@AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@user_id)
@HistoryManager = SandboxedModule.require modulePath, requires:
"request" : @request = sinon.stub()
"settings-sharelatex": @settings = {}
@settings.apis =
trackchanges:
enabled: false
url: "http://trackchanges.example.com"
project_history:
url: "http://project_history.example.com"
describe "initializeProject", ->
describe "with project history enabled", ->
beforeEach ->
@settings.apis.project_history.initializeHistoryForNewProjects = true
describe "project history returns a successful response", ->
beforeEach ->
@overleaf_id = 1234
@res = statusCode: 200
@body = JSON.stringify(project: id: @overleaf_id)
@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
@HistoryManager.initializeProject @callback
it "should call the project history api", ->
@request.post.calledWith(
url: "#{@settings.apis.project_history.url}/project"
).should.equal true
it "should return the callback with the overleaf id", ->
@callback.calledWithExactly(null, { @overleaf_id }).should.equal true
describe "project history returns a response without the project id", ->
beforeEach ->
@res = statusCode: 200
@body = JSON.stringify(project: {})
@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
@HistoryManager.initializeProject @callback
it "should return the callback with an error", ->
@callback
.calledWith(sinon.match.has("message", "project-history did not provide an id"))
.should.equal true
describe "project history returns a unsuccessful response", ->
beforeEach ->
@res = statusCode: 404
@request.post = sinon.stub().callsArgWith(1, null, @res)
@HistoryManager.initializeProject @callback
it "should return the callback with an error", ->
@callback
.calledWith(sinon.match.has("message", "project-history returned a non-success status code: 404"))
.should.equal true
describe "project history errors", ->
beforeEach ->
@error = sinon.stub()
@request.post = sinon.stub().callsArgWith(1, @error)
@HistoryManager.initializeProject @callback
it "should return the callback with the error", ->
@callback.calledWithExactly(@error).should.equal true
describe "with project history disabled", ->
beforeEach ->
@settings.apis.project_history.initializeHistoryForNewProjects = false
@HistoryManager.initializeProject @callback
it "should return the callback", ->
@callback.calledWithExactly().should.equal true

View file

@ -38,7 +38,7 @@ describe 'ProjectCreationHandler', ->
setRootDoc: sinon.stub().callsArg(2)
@ProjectDetailsHandler =
validateProjectName: sinon.stub().yields()
@HistoryController =
@HistoryManager =
initializeProject: sinon.stub().callsArg(0)
@user =
@ -53,7 +53,7 @@ describe 'ProjectCreationHandler', ->
'../../models/User': User:@User
'../../models/Project':{Project:@ProjectModel}
'../../models/Folder':{Folder:@FolderModel}
'../History/HistoryController': @HistoryController
'../History/HistoryManager': @HistoryManager
'./ProjectEntityHandler':@ProjectEntityHandler
"./ProjectDetailsHandler":@ProjectDetailsHandler
"settings-sharelatex": @Settings = {}
@ -66,7 +66,7 @@ describe 'ProjectCreationHandler', ->
describe 'Creating a Blank project', ->
beforeEach ->
@overleaf_id = 1234
@HistoryController.initializeProject = sinon.stub().callsArgWith(0, null, { @overleaf_id })
@HistoryManager.initializeProject = sinon.stub().callsArgWith(0, null, { @overleaf_id })
@ProjectModel::save = sinon.stub().callsArg(0)
describe "successfully", ->
@ -83,7 +83,7 @@ describe 'ProjectCreationHandler', ->
it "should initialize the project overleaf if history id not provided", (done)->
@handler.createBlankProject ownerId, projectName, done
@HistoryController.initializeProject.calledWith().should.equal true
@HistoryManager.initializeProject.calledWith().should.equal true
it "should set the overleaf id if overleaf id not provided", (done)->
@handler.createBlankProject ownerId, projectName, (err, project)=>

View file

@ -64,7 +64,7 @@ describe 'ProjectDuplicator', ->
@projectOptionsHandler =
setCompiler : sinon.stub()
@entityHandler =
addDocWithProject: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
addDoc: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(5)
setRootDoc: sinon.stub()
addFolderWithProject: sinon.stub().callsArgWith(3, null, @newFolder)
@ -112,13 +112,13 @@ describe 'ProjectDuplicator', ->
done()
it 'should use the same compiler', (done)->
@entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
@entityHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@projectOptionsHandler.setCompiler.calledWith(@stubbedNewProject._id, @project.compiler).should.equal true
done()
it 'should use the same root doc', (done)->
@entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
@entityHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@entityHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true
done()
@ -139,13 +139,13 @@ describe 'ProjectDuplicator', ->
it 'should copy all the docs', (done)->
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@DocstoreManager.getAllDocs.calledWith(@old_project_id).should.equal true
@entityHandler.addDocWithProject
@entityHandler.addDoc
.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines, @owner._id)
.should.equal true
@entityHandler.addDocWithProject
@entityHandler.addDoc
.calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines, @owner._id)
.should.equal true
@entityHandler.addDocWithProject
@entityHandler.addDoc
.calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines, @owner._id)
.should.equal true
done()

View file

@ -157,13 +157,13 @@ describe 'ProjectEntityHandler', ->
@ProjectGetter.getProject.callsArgWith(2, null, @project)
@tpdsUpdateSender.deleteEntity = sinon.stub().callsArg(1)
@ProjectEntityHandler._removeElementFromMongoArray = sinon.stub().callsArg(3)
@ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(3)
@ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(5)
@path = mongo: "mongo.path", fileSystem: "/file/system/path"
@projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: entity_id }, @path)
describe "deleting from Mongo", ->
beforeEach (done) ->
@ProjectEntityHandler.deleteEntity project_id, entity_id, @type = 'file', done
@ProjectEntityHandler.deleteEntity project_id, entity_id, @type = 'file', userId, done
it "should retreive the path", ->
@projectLocator.findElement.called.should.equal true
@ -182,7 +182,7 @@ describe 'ProjectEntityHandler', ->
it "should clean up the entity from the rest of the system", ->
@ProjectEntityHandler._cleanUpEntity
.calledWith(@project, @entity, @type)
.calledWith(@project, @entity, @type, @path.fileSystem, userId)
.should.equal true
describe "_cleanUpEntity", ->
@ -193,7 +193,9 @@ describe 'ProjectEntityHandler', ->
describe "a file", ->
beforeEach (done) ->
@ProjectEntityHandler._cleanUpEntity @project, _id: @entity_id, 'file', done
@path = "/file/system/path.png"
@entity = _id: @entity_id
@ProjectEntityHandler._cleanUpEntity @project, @entity, 'file', @path, userId, done
it "should delete the file from FileStoreHandler", ->
@FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal true
@ -201,38 +203,56 @@ describe 'ProjectEntityHandler', ->
it "should not attempt to delete from the document updater", ->
@documentUpdaterHandler.deleteDoc.called.should.equal false
it "should should send the update to the doc updater", ->
oldFiles = [ file: @entity, path: @path ]
@documentUpdaterHandler.updateProjectStructure
.calledWith(project_id, userId, {oldFiles})
.should.equal true
describe "a doc", ->
beforeEach (done) ->
@ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(2)
@ProjectEntityHandler._cleanUpEntity @project, @entity = {_id: @entity_id}, 'doc', done
@path = "/file/system/path.tex"
@ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4)
@entity = {_id: @entity_id}
@ProjectEntityHandler._cleanUpEntity @project, @entity, 'doc', @path, userId, done
it "should clean up the doc", ->
@ProjectEntityHandler._cleanUpDoc
.calledWith(@project, @entity)
.calledWith(@project, @entity, @path, userId)
.should.equal true
describe "a folder", ->
beforeEach (done) ->
@folder =
folders: [
fileRefs: [ @file1 = {_id: "file-id-1" } ]
docs: [ @doc1 = { _id: "doc-id-1" } ]
name: "subfolder"
fileRefs: [ @file1 = { _id: "file-id-1", name: "file-name-1"} ]
docs: [ @doc1 = { _id: "doc-id-1", name: "doc-name-1" } ]
folders: []
]
fileRefs: [ @file2 = { _id: "file-id-2" } ]
docs: [ @doc2 = { _id: "doc-id-2" } ]
fileRefs: [ @file2 = { _id: "file-id-2", name: "file-name-2" } ]
docs: [ @doc2 = { _id: "doc-id-2", name: "doc-name-2" } ]
@ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(2)
@ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(2)
@ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", done
@ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4)
@ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(4)
path = "/folder"
@ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", path, userId, done
it "should clean up all sub files", ->
@ProjectEntityHandler._cleanUpFile.calledWith(@project, @file1).should.equal true
@ProjectEntityHandler._cleanUpFile.calledWith(@project, @file2).should.equal true
@ProjectEntityHandler._cleanUpFile
.calledWith(@project, @file1, "/folder/subfolder/file-name-1", userId)
.should.equal true
@ProjectEntityHandler._cleanUpFile
.calledWith(@project, @file2, "/folder/file-name-2", userId)
.should.equal true
it "should clean up all sub docs", ->
@ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc1).should.equal true
@ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc2).should.equal true
@ProjectEntityHandler._cleanUpDoc
.calledWith(@project, @doc1, "/folder/subfolder/doc-name-1", userId)
.should.equal true
@ProjectEntityHandler._cleanUpDoc
.calledWith(@project, @doc2, "/folder/doc-name-2", userId)
.should.equal true
describe 'moveEntity', ->
beforeEach ->
@ -496,6 +516,51 @@ describe 'ProjectEntityHandler', ->
.calledWith(project_id, userId, {newDocs})
.should.equal true
describe 'addDocWithoutUpdatingHistory', ->
beforeEach ->
@name = "some new doc"
@lines = ['1234','abc']
@path = "/path/to/doc"
@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:@path}})
@callback = sinon.stub()
@tpdsUpdateSender.addDoc = sinon.stub().callsArg(1)
@DocstoreManager.updateDoc = sinon.stub().yields(null, true, 0)
@ProjectEntityHandler.addDocWithoutUpdatingHistory project_id, folder_id, @name, @lines, userId, @callback
# Created doc
@doc = @ProjectEntityHandler._putElement.args[0][2]
@doc.name.should.equal @name
expect(@doc.lines).to.be.undefined
it 'should call put element', ->
@ProjectEntityHandler._putElement.calledWith(@project, folder_id, @doc).should.equal true
it 'should return doc and parent folder', ->
@callback.calledWith(null, @doc, folder_id).should.equal true
it 'should call third party data store', ->
@tpdsUpdateSender.addDoc
.calledWith({
project_id: project_id
doc_id: doc_id
path: @path
project_name: @project.name
rev: 0
})
.should.equal true
it "should send the doc lines to the doc store", ->
@DocstoreManager.updateDoc
.calledWith(project_id, @doc._id.toString(), @lines)
.should.equal true
it "should not should send the change in project structure to the doc updater", () ->
@documentUpdaterHandler.updateProjectStructure
.called
.should.equal false
describe "restoreDoc", ->
beforeEach ->
@name = "doc-name"
@ -584,6 +649,12 @@ describe 'ProjectEntityHandler', ->
@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, () ->
it "should not send the change in project structure to the doc updater when called as addFileWithoutUpdatingHistory", (done) ->
@documentUpdaterHandler.updateProjectStructure = sinon.stub().yields()
@ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, {}, userId, () =>
@documentUpdaterHandler.updateProjectStructure.called.should.equal false
done()
describe 'replaceFile', ->
beforeEach ->
@projectLocator
@ -1116,6 +1187,7 @@ describe 'ProjectEntityHandler', ->
@doc =
_id: ObjectId()
name: "test.tex"
@path = "/path/to/doc"
@ProjectEntityHandler.unsetRootDoc = sinon.stub().callsArg(1)
@ProjectEntityHandler._insertDeletedDocReference = sinon.stub().callsArg(2)
@documentUpdaterHandler.deleteDoc = sinon.stub().callsArg(2)
@ -1125,7 +1197,7 @@ describe 'ProjectEntityHandler', ->
describe "when the doc is the root doc", ->
beforeEach ->
@project.rootDoc_id = @doc._id
@ProjectEntityHandler._cleanUpDoc @project, @doc, @callback
@ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback
it "should unset the root doc", ->
@ProjectEntityHandler.unsetRootDoc
@ -1146,13 +1218,19 @@ describe 'ProjectEntityHandler', ->
.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, userId, {oldDocs})
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
describe "when the doc is not the root doc", ->
beforeEach ->
@project.rootDoc_id = ObjectId()
@ProjectEntityHandler._cleanUpDoc @project, @doc, @callback
@ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback
it "should not unset the root doc", ->
@ProjectEntityHandler.unsetRootDoc.called.should.equal false

View file

@ -57,6 +57,7 @@ describe "SubscriptionUpdater", ->
@ReferalAllocator = assignBonus:sinon.stub().callsArgWith(1)
@ReferalAllocator.cock = true
@Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, null)}}
@SubscriptionUpdater = SandboxedModule.require modulePath, requires:
'../../models/Subscription': Subscription:@SubscriptionModel
'./UserFeaturesUpdater': @UserFeaturesUpdater
@ -65,6 +66,7 @@ describe "SubscriptionUpdater", ->
"logger-sharelatex": log:->
'settings-sharelatex': @Settings
"../Referal/ReferalAllocator" : @ReferalAllocator
'../../infrastructure/Modules': @Modules
describe "syncSubscription", ->
@ -204,10 +206,22 @@ describe "SubscriptionUpdater", ->
assert.equal args[1], @groupSubscription.planCode
done()
it "should call updateFeatures with the overleaf subscription if set", (done)->
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, null)
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, ['ol_pro'])
@SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
assert.equal args[1], 'ol_pro'
done()
it "should call not call updateFeatures with users subscription if the subscription plan code is the default one (downgraded)", (done)->
@subscription.planCode = @Settings.defaultPlanCode
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription)
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null)
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
@ -218,6 +232,7 @@ describe "SubscriptionUpdater", ->
it "should call updateFeatures with default if there are no subscriptions for user", (done)->
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null)
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null)
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
@ -263,3 +278,13 @@ describe "SubscriptionUpdater", ->
@SubscriptionUpdater._setUsersMinimumFeatures
.calledWith(user_id)
.should.equal true
describe 'refreshSubscription', ->
beforeEach ->
@SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub()
.callsArgWith(1, null)
it 'should call to _setUsersMinimumFeatures', ->
@SubscriptionUpdater.refreshSubscription(@adminUser._id, ()->)
@SubscriptionUpdater._setUsersMinimumFeatures.callCount.should.equal 1
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true

View file

@ -8,7 +8,7 @@ describe 'TpdsUpdateHandler', ->
beforeEach ->
@requestQueuer = {}
@updateMerger =
deleteUpdate: (user_id, path, source, cb)->cb()
deleteUpdate: (user_id, project_id, path, source, cb)->cb()
mergeUpdate:(user_id, project_id, path, update, source, cb)->cb()
@editorController = {}
@project_id = "dsjajilknaksdn"
@ -107,11 +107,13 @@ describe 'TpdsUpdateHandler', ->
it 'should call deleteEntity in the collaberation manager', (done)->
path = "/delete/this"
update = {}
@updateMerger.deleteUpdate = sinon.stub().callsArg(3)
@updateMerger.deleteUpdate = sinon.stub().callsArg(4)
@handler.deleteUpdate @user_id, @project.name, path, @source, =>
@projectDeleter.markAsDeletedByExternalSource.calledWith(@project._id).should.equal false
@updateMerger.deleteUpdate.calledWith(@project_id, path, @source).should.equal true
@updateMerger.deleteUpdate
.calledWith(@user_id, @project_id, path, @source)
.should.equal true
done()
it 'should mark the project as deleted by external source if path is a single slash', (done)->

View file

@ -145,13 +145,13 @@ describe 'UpdateMerger :', ->
it 'should get the element id', ->
@projectLocator.findElementByPath = sinon.spy()
@updateMerger.deleteUpdate @project_id, @path, @source, ->
@updateMerger.deleteUpdate @user_id, @project_id, @path, @source, ->
@projectLocator.findElementByPath.calledWith(@project_id, @path).should.equal true
it 'should delete the entity in the editor controller with the correct type', (done)->
@entity.lines = []
mock = sinon.mock(@editorController).expects("deleteEntity").withArgs(@project_id, @entity_id, @type, @source).callsArg(4)
@updateMerger.deleteUpdate @project_id, @path, @source, ->
mock = sinon.mock(@editorController).expects("deleteEntity").withArgs(@project_id, @entity_id, @type, @source, @user_id).callsArg(5)
@updateMerger.deleteUpdate @user_id, @project_id, @path, @source, ->
mock.verify()
done()

View file

@ -0,0 +1,134 @@
Path = require 'path'
SandboxedModule = require "sandboxed-module"
modulePath = Path.join __dirname, '../../../public/js/ide/history/HistoryV2Manager'
sinon = require("sinon")
expect = require("chai").expect
describe "HistoryV2Manager", ->
beforeEach ->
@moment = {}
@ColorManager = {}
SandboxedModule.require modulePath, globals:
"define": (dependencies, builder) =>
@HistoryV2Manager = builder(@moment, @ColorManager)
@scope =
$watch: sinon.stub()
$on: sinon.stub()
@ide = {}
@historyManager = new @HistoryV2Manager(@ide, @scope)
it "should setup the history scope on intialization", ->
expect(@scope.history).to.deep.equal({
isV2: true
updates: []
nextBeforeTimestamp: null
atEnd: false
selection: {
updates: []
pathname: null
range: {
fromV: null
toV: null
}
}
diff: null
})
describe "_perDocSummaryOfUpdates", ->
it "should return the range of updates for the docs", ->
result = @historyManager._perDocSummaryOfUpdates([{
pathnames: ["main.tex"]
fromV: 7, toV: 9
},{
pathnames: ["main.tex", "foo.tex"]
fromV: 4, toV: 6
},{
pathnames: ["main.tex"]
fromV: 3, toV: 3
},{
pathnames: ["foo.tex"]
fromV: 0, toV: 2
}])
expect(result).to.deep.equal({
"main.tex": { fromV: 3, toV: 9 },
"foo.tex": { fromV: 0, toV: 6 }
})
it "should track renames", ->
result = @historyManager._perDocSummaryOfUpdates([{
pathnames: ["main2.tex"]
fromV: 5, toV: 9
},{
project_ops: [{
rename: {
pathname: "main1.tex",
newPathname: "main2.tex"
}
}],
fromV: 4, toV: 4
},{
pathnames: ["main1.tex"]
fromV: 3, toV: 3
},{
project_ops: [{
rename: {
pathname: "main0.tex",
newPathname: "main1.tex"
}
}],
fromV: 2, toV: 2
},{
pathnames: ["main0.tex"]
fromV: 0, toV: 1
}])
expect(result).to.deep.equal({
"main0.tex": { fromV: 0, toV: 9 }
})
it "should track single renames", ->
result = @historyManager._perDocSummaryOfUpdates([{
project_ops: [{
rename: {
pathname: "main1.tex",
newPathname: "main2.tex"
}
}],
fromV: 4, toV: 5
}])
expect(result).to.deep.equal({
"main1.tex": { fromV: 4, toV: 5 }
})
it "should track additions", ->
result = @historyManager._perDocSummaryOfUpdates([{
project_ops: [{
add:
pathname: "main.tex"
}]
fromV: 0, toV: 1
}, {
pathnames: ["main.tex"]
fromV: 1, toV: 4
}])
expect(result).to.deep.equal({
"main.tex": { fromV: 0, toV: 4 }
})
it "should track single additions", ->
result = @historyManager._perDocSummaryOfUpdates([{
project_ops: [{
add:
pathname: "main.tex"
}]
fromV: 0, toV: 1
}])
expect(result).to.deep.equal({
"main.tex": { fromV: 0, toV: 1 }
})