mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-09 13:36:46 +00:00
Merge remote-tracking branch 'origin/master' into afc-email-tokens
This commit is contained in:
commit
25d7196570
67 changed files with 1207 additions and 413 deletions
|
@ -1,5 +1,5 @@
|
|||
BetaProgramHandler = require './BetaProgramHandler'
|
||||
UserLocator = require "../User/UserLocator"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
Settings = require "settings-sharelatex"
|
||||
logger = require 'logger-sharelatex'
|
||||
AuthenticationController = require '../Authentication/AuthenticationController'
|
||||
|
@ -30,7 +30,7 @@ module.exports = BetaProgramController =
|
|||
optInPage: (req, res, next)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {user_id}, "showing beta participation page for user"
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
UserGetter.getUser user_id, (err, user)->
|
||||
if err
|
||||
logger.err {err, user_id}, "error fetching user"
|
||||
return next(err)
|
||||
|
|
|
@ -27,7 +27,7 @@ module.exports = CollaboratorsInviteController =
|
|||
_checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) ->
|
||||
if Settings.restrictInvitesToExistingAccounts == true
|
||||
logger.log {email}, "checking if user exists with this email"
|
||||
UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
|
||||
UserGetter.getUserByMainEmail email, {_id: 1}, (err, user) ->
|
||||
return callback(err) if err?
|
||||
userExists = user? and user?._id?
|
||||
callback(null, userExists)
|
||||
|
|
|
@ -32,7 +32,7 @@ module.exports = CollaboratorsInviteHandler =
|
|||
|
||||
_trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) ->
|
||||
email = invite.email
|
||||
UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) ->
|
||||
UserGetter.getUserByMainEmail email, {_id: 1}, (err, existingUser) ->
|
||||
if err?
|
||||
logger.err {projectId, email}, "error checking if user exists"
|
||||
return callback(err)
|
||||
|
|
|
@ -5,7 +5,8 @@ logger = require 'logger-sharelatex'
|
|||
|
||||
module.exports = LinkedFilesController = {
|
||||
Agents: {
|
||||
url: require('./UrlAgent')
|
||||
url: require('./UrlAgent'),
|
||||
project_file: require('./ProjectFileAgent')
|
||||
}
|
||||
|
||||
createLinkedFile: (req, res, next) ->
|
||||
|
@ -22,11 +23,17 @@ module.exports = LinkedFilesController = {
|
|||
|
||||
linkedFileData = Agent.sanitizeData(data)
|
||||
linkedFileData.provider = provider
|
||||
Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) ->
|
||||
if error?
|
||||
logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk'
|
||||
return Agent.handleError(error, req, res, next)
|
||||
EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.send(204) # created
|
||||
}
|
||||
Agent.checkAuth project_id, linkedFileData, user_id, (err, allowed) ->
|
||||
return Agent.handleError(err, req, res, next) if err?
|
||||
return res.sendStatus(403) if !allowed
|
||||
Agent.decorateLinkedFileData linkedFileData, (err, newLinkedFileData) ->
|
||||
return Agent.handleError(err) if err?
|
||||
linkedFileData = newLinkedFileData
|
||||
Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) ->
|
||||
if error?
|
||||
logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk'
|
||||
return Agent.handleError(error, req, res, next)
|
||||
EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error, file) ->
|
||||
return next(error) if error?
|
||||
res.json(new_file_id: file._id) # created
|
||||
}
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
FileWriter = require('../../infrastructure/FileWriter')
|
||||
AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
ProjectLocator = require('../Project/ProjectLocator')
|
||||
ProjectGetter = require('../Project/ProjectGetter')
|
||||
DocstoreManager = require('../Docstore/DocstoreManager')
|
||||
FileStoreHandler = require('../FileStore/FileStoreHandler')
|
||||
FileWriter = require('../../infrastructure/FileWriter')
|
||||
_ = require "underscore"
|
||||
Settings = require 'settings-sharelatex'
|
||||
|
||||
|
||||
AccessDeniedError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'AccessDenied'
|
||||
error.__proto__ = AccessDeniedError.prototype
|
||||
return error
|
||||
AccessDeniedError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
BadEntityTypeError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadEntityType'
|
||||
error.__proto__ = BadEntityTypeError.prototype
|
||||
return error
|
||||
BadEntityTypeError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
BadDataError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadData'
|
||||
error.__proto__ = BadDataError.prototype
|
||||
return error
|
||||
BadDataError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
ProjectNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'ProjectNotFound'
|
||||
error.__proto__ = ProjectNotFoundError.prototype
|
||||
return error
|
||||
ProjectNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
SourceFileNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadData'
|
||||
error.__proto__ = SourceFileNotFoundError.prototype
|
||||
return error
|
||||
SourceFileNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
module.exports = ProjectFileAgent =
|
||||
|
||||
sanitizeData: (data) ->
|
||||
return _.pick(
|
||||
data,
|
||||
'source_project_id',
|
||||
'source_entity_path'
|
||||
)
|
||||
|
||||
_validate: (data) ->
|
||||
return (
|
||||
data.source_project_id? &&
|
||||
data.source_entity_path?
|
||||
)
|
||||
|
||||
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
|
||||
callback = _.once(callback)
|
||||
{ source_project_id } = data
|
||||
return callback(new BadDataError()) if !source_project_id?
|
||||
ProjectGetter.getProject source_project_id, (err, project) ->
|
||||
return callback(err) if err?
|
||||
return callback(new ProjectNotFoundError()) if !project?
|
||||
callback(err, _.extend(data, {source_project_display_name: project.name}))
|
||||
|
||||
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
|
||||
callback = _.once(callback)
|
||||
if !ProjectFileAgent._validate(data)
|
||||
return callback(new BadDataError())
|
||||
{source_project_id, source_entity_path} = data
|
||||
AuthorizationManager.canUserReadProject current_user_id, source_project_id, null, (err, canRead) ->
|
||||
return callback(err) if err?
|
||||
callback(null, canRead)
|
||||
|
||||
writeIncomingFileToDisk:
|
||||
(project_id, data, current_user_id, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
if !ProjectFileAgent._validate(data)
|
||||
return callback(new BadDataError())
|
||||
{source_project_id, source_entity_path} = data
|
||||
ProjectLocator.findElementByPath {
|
||||
project_id: source_project_id,
|
||||
path: source_entity_path
|
||||
}, (err, entity, type) ->
|
||||
if err?
|
||||
if err.toString().match(/^not found.*/)
|
||||
err = new SourceFileNotFoundError()
|
||||
return callback(err)
|
||||
ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback
|
||||
|
||||
_writeEntityToDisk: (project_id, entity_id, type, callback=(err, location)->) ->
|
||||
callback = _.once(callback)
|
||||
if type == 'doc'
|
||||
DocstoreManager.getDoc project_id, entity_id, (err, lines) ->
|
||||
return callback(err) if err?
|
||||
FileWriter.writeLinesToDisk entity_id, lines, callback
|
||||
else if type == 'file'
|
||||
FileStoreHandler.getFileStream project_id, entity_id, null, (err, fileStream) ->
|
||||
return callback(err) if err?
|
||||
FileWriter.writeStreamToDisk entity_id, fileStream, callback
|
||||
else
|
||||
callback(new BadEntityTypeError())
|
||||
|
||||
handleError: (error, req, res, next) ->
|
||||
if error instanceof AccessDeniedError
|
||||
res.status(403).send("You do not have access to this project")
|
||||
else if error instanceof BadDataError
|
||||
res.status(400).send("The submitted data is not valid")
|
||||
else if error instanceof BadEntityTypeError
|
||||
res.status(400).send("The file is the wrong type")
|
||||
else if error instanceof SourceFileNotFoundError
|
||||
res.status(404).send("Source file not found")
|
||||
else if error instanceof ProjectNotFoundError
|
||||
res.status(404).send("Project not found")
|
||||
else
|
||||
next(error)
|
||||
next()
|
|
@ -27,6 +27,12 @@ module.exports = UrlAgent = {
|
|||
url: @._prependHttpIfNeeded(data.url)
|
||||
}
|
||||
|
||||
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
|
||||
return callback(null, data)
|
||||
|
||||
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
|
||||
callback(null, true)
|
||||
|
||||
writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
url = data.url
|
||||
|
@ -65,4 +71,4 @@ module.exports = UrlAgent = {
|
|||
if !Settings.apis?.linkedUrlProxy?.url?
|
||||
throw new Error('no linked url proxy configured')
|
||||
return "#{Settings.apis.linkedUrlProxy.url}?url=#{encodeURIComponent(url)}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ logger = require("logger-sharelatex")
|
|||
module.exports =
|
||||
|
||||
generateAndEmailResetToken:(email, callback = (error, exists) ->)->
|
||||
UserGetter.getUser email:email, (err, user)->
|
||||
UserGetter.getUserByMainEmail email, (err, user)->
|
||||
if err then return callback(err)
|
||||
if !user? or user.holdingAccount
|
||||
logger.err email:email, "user could not be found for password reset"
|
||||
|
|
|
@ -25,6 +25,7 @@ Sources = require "../Authorization/Sources"
|
|||
TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
|
||||
CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
|
||||
Modules = require '../../infrastructure/Modules'
|
||||
ProjectEntityHandler = require './ProjectEntityHandler'
|
||||
crypto = require 'crypto'
|
||||
|
||||
module.exports = ProjectController =
|
||||
|
@ -138,6 +139,33 @@ module.exports = ProjectController =
|
|||
return next(err) if err?
|
||||
res.sendStatus 200
|
||||
|
||||
userProjectsJson: (req, res, next) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
ProjectGetter.findAllUsersProjects user_id,
|
||||
'name lastUpdated publicAccesLevel archived owner_ref tokens', (err, projects) ->
|
||||
return next(err) if err?
|
||||
projects = ProjectController._buildProjectList(projects)
|
||||
.filter((p) -> !p.archived)
|
||||
.filter((p) -> !p.isV1Project)
|
||||
.map((p) -> {_id: p.id, name: p.name, accessLevel: p.accessLevel})
|
||||
|
||||
res.json({projects: projects})
|
||||
|
||||
projectEntitiesJson: (req, res, next) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
project_id = req.params.Project_id
|
||||
ProjectGetter.getProject project_id, (err, project) ->
|
||||
return next(err) if err?
|
||||
ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) ->
|
||||
return next(err) if err?
|
||||
entities = docs.concat(files)
|
||||
.sort (a, b) -> a.path > b.path # Sort by path ascending
|
||||
.map (e) -> {
|
||||
path: e.path,
|
||||
type: if e.doc? then 'doc' else 'file'
|
||||
}
|
||||
res.json({project_id: project_id, entities: entities})
|
||||
|
||||
projectListPage: (req, res, next)->
|
||||
timer = new metrics.Timer("project-list")
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
|
@ -313,6 +341,7 @@ module.exports = ProjectController =
|
|||
maxDocLength: Settings.max_doc_length
|
||||
useV2History: !!project.overleaf?.history?.display
|
||||
showRichText: req.query?.rt == 'true'
|
||||
showTestControls: req.query?.tc == 'true' || user.isAdmin
|
||||
showPublishModal: req.query?.pm == 'true'
|
||||
timer.done()
|
||||
|
||||
|
|
|
@ -45,37 +45,40 @@ wrapWithLock = (methodWithoutLock) ->
|
|||
methodWithLock
|
||||
|
||||
module.exports = ProjectEntityUpdateHandler = self =
|
||||
# this doesn't need any locking because it's only called by ProjectDuplicator
|
||||
copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
|
||||
project_id = project._id
|
||||
projectHistoryId = project.overleaf?.history?.id
|
||||
logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
|
||||
return callback(err) if err?
|
||||
ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id)=>
|
||||
if !origonalFileRef?
|
||||
logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
|
||||
return callback()
|
||||
# convert any invalid characters in original file to '_'
|
||||
fileRef = new File name : SafePath.clean(origonalFileRef.name)
|
||||
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
|
||||
return callback(err)
|
||||
ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id }, "error putting element as part of copy"
|
||||
return callback(err)
|
||||
TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
|
||||
copyFileFromExistingProjectWithProject: wrapWithLock
|
||||
beforeLock: (next) ->
|
||||
(project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
|
||||
project_id = project._id
|
||||
logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
|
||||
ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id) ->
|
||||
if !origonalFileRef?
|
||||
logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
|
||||
return callback()
|
||||
# convert any invalid characters in original file to '_'
|
||||
fileRef = new File name : SafePath.clean(origonalFileRef.name)
|
||||
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
|
||||
newFiles = [
|
||||
file: fileRef
|
||||
path: result?.path?.fileSystem
|
||||
url: fileStoreUrl
|
||||
]
|
||||
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles}, (error) ->
|
||||
return callback(error) if error?
|
||||
callback null, fileRef, folder_id
|
||||
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
|
||||
return callback(err)
|
||||
next(project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback)
|
||||
withLock: (project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)->
|
||||
project_id = project._id
|
||||
projectHistoryId = project.overleaf?.history?.id
|
||||
ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result, newProject) ->
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id }, "error putting element as part of copy"
|
||||
return callback(err)
|
||||
TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
|
||||
newFiles = [
|
||||
file: fileRef
|
||||
path: result?.path?.fileSystem
|
||||
url: fileStoreUrl
|
||||
]
|
||||
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles, newProject}, (error) ->
|
||||
return callback(error) if error?
|
||||
callback null, fileRef, folder_id
|
||||
|
||||
updateDocLines: (project_id, doc_id, lines, version, ranges, callback = (error) ->)->
|
||||
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
|
||||
|
|
|
@ -10,6 +10,8 @@ Async = require('async')
|
|||
oneMinInMs = 60 * 1000
|
||||
fiveMinsInMs = oneMinInMs * 5
|
||||
|
||||
if !settings.apis?.references?.url?
|
||||
logger.log "references search not enabled"
|
||||
|
||||
module.exports = ReferencesHandler =
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ async = require("async")
|
|||
_ = require("underscore")
|
||||
SubscriptionUpdater = require("./SubscriptionUpdater")
|
||||
SubscriptionLocator = require("./SubscriptionLocator")
|
||||
UserLocator = require("../User/UserLocator")
|
||||
UserGetter = require("../User/UserGetter")
|
||||
LimitationsManager = require("./LimitationsManager")
|
||||
logger = require("logger-sharelatex")
|
||||
OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
|
||||
|
@ -22,7 +22,7 @@ module.exports = SubscriptionGroupHandler =
|
|||
if limitReached
|
||||
logger.err adminUserId:adminUserId, newEmail:newEmail, "group subscription limit reached not adding user to group"
|
||||
return callback(limitReached:limitReached)
|
||||
UserLocator.findByEmail newEmail, (err, user)->
|
||||
UserGetter.getUserByMainEmail newEmail, (err, user)->
|
||||
return callback(err) if err?
|
||||
if user?
|
||||
SubscriptionUpdater.addUserToGroup adminUserId, user._id, (err)->
|
||||
|
@ -52,7 +52,7 @@ module.exports = SubscriptionGroupHandler =
|
|||
|
||||
jobs = _.map subscription.member_ids, (user_id)->
|
||||
return (cb)->
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
UserGetter.getUser user_id, (err, user)->
|
||||
if err? or !user?
|
||||
users.push _id:user_id
|
||||
return cb()
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
path = require('path')
|
||||
Project = require('../../../js/models/Project').Project
|
||||
ProjectUploadManager = require('../../../js/Features/Uploads/ProjectUploadManager')
|
||||
ProjectOptionsHandler = require('../../../js/Features/Project/ProjectOptionsHandler')
|
||||
AuthenticationController = require('../../../js/Features/Authentication/AuthenticationController')
|
||||
settings = require('settings-sharelatex')
|
||||
fs = require('fs')
|
||||
request = require('request')
|
||||
uuid = require('uuid')
|
||||
logger = require('logger-sharelatex')
|
||||
async = require("async")
|
||||
|
||||
|
||||
module.exports = TemplatesController =
|
||||
|
||||
getV1Template: (req, res)->
|
||||
templateVersionId = req.params.Template_version_id
|
||||
templateId = req.query.id
|
||||
if !/^[0-9]+$/.test(templateVersionId) || !/^[0-9]+$/.test(templateId)
|
||||
logger.err templateVersionId:templateVersionId, templateId: templateId, "invalid template id or version"
|
||||
return res.sendStatus 400
|
||||
data = {}
|
||||
data.templateVersionId = templateVersionId
|
||||
data.templateId = templateId
|
||||
data.name = req.query.templateName
|
||||
data.compiler = req.query.latexEngine
|
||||
res.render path.resolve(__dirname, "../../../views/project/editor/new_from_template"), data
|
||||
|
||||
createProjectFromV1Template: (req, res)->
|
||||
currentUserId = AuthenticationController.getLoggedInUserId(req)
|
||||
zipUrl = "#{settings.apis.v1.url}/api/v1/sharelatex/templates/#{req.body.templateVersionId}"
|
||||
zipReq = request(zipUrl, {
|
||||
'auth': {
|
||||
'user': settings.apis.v1.user,
|
||||
'pass': settings.apis.v1.pass
|
||||
}
|
||||
})
|
||||
|
||||
TemplatesController.createFromZip(
|
||||
zipReq,
|
||||
{
|
||||
templateName: req.body.templateName,
|
||||
currentUserId: currentUserId,
|
||||
compiler: req.body.compiler
|
||||
docId: req.body.docId
|
||||
templateId: req.body.templateId
|
||||
templateVersionId: req.body.templateVersionId
|
||||
},
|
||||
req,
|
||||
res
|
||||
)
|
||||
|
||||
createFromZip: (zipReq, options, req, res)->
|
||||
dumpPath = "#{settings.path.dumpFolder}/#{uuid.v4()}"
|
||||
writeStream = fs.createWriteStream(dumpPath)
|
||||
|
||||
zipReq.on "error", (error) ->
|
||||
logger.error err: error, "error getting zip from template API"
|
||||
zipReq.pipe(writeStream)
|
||||
writeStream.on 'close', ->
|
||||
ProjectUploadManager.createProjectFromZipArchive options.currentUserId, options.templateName, dumpPath, (err, project)->
|
||||
if err?
|
||||
logger.err err:err, zipReq:zipReq, "problem building project from zip"
|
||||
return res.sendStatus 500
|
||||
setCompiler project._id, options.compiler, ->
|
||||
fs.unlink dumpPath, ->
|
||||
delete req.session.templateData
|
||||
conditions = {_id:project._id}
|
||||
update = {
|
||||
fromV1TemplateId:options.templateId,
|
||||
fromV1TemplateVersionId:options.templateVersionId
|
||||
}
|
||||
Project.update conditions, update, {}, (err)->
|
||||
res.redirect "/project/#{project._id}"
|
||||
|
||||
setCompiler = (project_id, compiler, callback)->
|
||||
if compiler?
|
||||
ProjectOptionsHandler.setCompiler project_id, compiler, callback
|
||||
else
|
||||
callback()
|
|
@ -0,0 +1,8 @@
|
|||
settings = require("settings-sharelatex")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
module.exports =
|
||||
saveTemplateDataInSession: (req, res, next)->
|
||||
if req.query.templateName
|
||||
req.session.templateData = req.query
|
||||
next()
|
|
@ -0,0 +1,9 @@
|
|||
settings = require("settings-sharelatex")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
|
||||
module.exports =
|
||||
saveTemplateDataInSession: (req, res, next)->
|
||||
if req.query.templateName
|
||||
req.session.templateData = req.query
|
||||
next()
|
|
@ -0,0 +1,10 @@
|
|||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
TemplatesController = require("./TemplatesController")
|
||||
TemplatesMiddlewear = require('./TemplatesMiddlewear')
|
||||
|
||||
module.exports =
|
||||
apply: (app)->
|
||||
|
||||
app.get '/project/new/template/:Template_version_id', TemplatesMiddlewear.saveTemplateDataInSession, AuthenticationController.requireLogin(), TemplatesController.getV1Template
|
||||
|
||||
app.post '/project/new/template', AuthenticationController.requireLogin(), TemplatesController.createProjectFromV1Template
|
|
@ -1,6 +1,6 @@
|
|||
UserHandler = require("./UserHandler")
|
||||
UserDeleter = require("./UserDeleter")
|
||||
UserLocator = require("./UserLocator")
|
||||
UserGetter = require("./UserGetter")
|
||||
User = require("../../models/User").User
|
||||
newsLetterManager = require('../Newsletter/NewsletterManager')
|
||||
UserRegistrationHandler = require("./UserRegistrationHandler")
|
||||
|
@ -45,7 +45,7 @@ module.exports = UserController =
|
|||
|
||||
unsubscribe: (req, res)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
UserGetter.getUser user_id, (err, user)->
|
||||
newsLetterManager.unsubscribe user, ->
|
||||
res.send()
|
||||
|
||||
|
|
|
@ -1,19 +1,10 @@
|
|||
User = require("../../models/User").User
|
||||
UserLocator = require("./UserLocator")
|
||||
logger = require("logger-sharelatex")
|
||||
metrics = require('metrics-sharelatex')
|
||||
|
||||
|
||||
module.exports = UserCreator =
|
||||
|
||||
getUserOrCreateHoldingAccount: (email, callback = (err, user)->)->
|
||||
self = @
|
||||
UserLocator.findByEmail email, (err, user)->
|
||||
if user?
|
||||
callback(err, user)
|
||||
else
|
||||
self.createNewUser email:email, holdingAccount:true, callback
|
||||
|
||||
createNewUser: (opts, callback)->
|
||||
logger.log opts:opts, "creating new user"
|
||||
user = new User()
|
||||
|
|
|
@ -6,6 +6,8 @@ ObjectId = mongojs.ObjectId
|
|||
|
||||
module.exports = UserGetter =
|
||||
getUser: (query, projection, callback = (error, user) ->) ->
|
||||
if query?.email?
|
||||
return callback(new Error("Don't use getUser to find user by email"), null)
|
||||
if arguments.length == 2
|
||||
callback = projection
|
||||
projection = {}
|
||||
|
@ -19,6 +21,13 @@ module.exports = UserGetter =
|
|||
|
||||
db.users.findOne query, projection, callback
|
||||
|
||||
getUserByMainEmail: (email, projection, callback = (error, user) ->) ->
|
||||
email = email.trim()
|
||||
if arguments.length == 2
|
||||
callback = projection
|
||||
projection = {}
|
||||
db.users.findOne email: email, projection, callback
|
||||
|
||||
getUsers: (user_ids, projection, callback = (error, users) ->) ->
|
||||
try
|
||||
user_ids = user_ids.map (u) -> ObjectId(u.toString())
|
||||
|
@ -39,6 +48,7 @@ module.exports = UserGetter =
|
|||
|
||||
[
|
||||
'getUser',
|
||||
'getUserByMainEmail',
|
||||
'getUsers',
|
||||
'getUserOrUserStubById'
|
||||
].map (method) ->
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
mongojs = require("../../infrastructure/mongojs")
|
||||
metrics = require("metrics-sharelatex")
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
logger = require('logger-sharelatex')
|
||||
|
||||
module.exports = UserLocator =
|
||||
|
||||
findByEmail: (email, callback)->
|
||||
email = email.trim()
|
||||
db.users.findOne email:email, (err, user)->
|
||||
callback(err, user)
|
||||
|
||||
findById: (_id, callback)->
|
||||
db.users.findOne _id:ObjectId(_id+""), callback
|
||||
|
||||
[
|
||||
'findById',
|
||||
'findByEmail'
|
||||
].map (method) ->
|
||||
metrics.timeAsyncMethod UserLocator, method, 'mongo.UserLocator', logger
|
|
@ -1,4 +1,3 @@
|
|||
UserLocator = require("./UserLocator")
|
||||
UserGetter = require("./UserGetter")
|
||||
UserSessionsManager = require("./UserSessionsManager")
|
||||
ErrorController = require("../Errors/ErrorController")
|
||||
|
@ -61,7 +60,7 @@ module.exports =
|
|||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log user: user_id, "loading settings page"
|
||||
shouldAllowEditingDetails = !(Settings?.ldap?.updateUserDetailsOnLogin) and !(Settings?.saml?.updateUserDetailsOnLogin)
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
UserGetter.getUser user_id, (err, user)->
|
||||
return next(err) if err?
|
||||
res.render 'user/settings',
|
||||
title:'account_settings'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
sanitize = require('sanitizer')
|
||||
User = require("../../models/User").User
|
||||
UserCreator = require("./UserCreator")
|
||||
UserGetter = require("./UserGetter")
|
||||
AuthenticationManager = require("../Authentication/AuthenticationManager")
|
||||
NewsLetterManager = require("../Newsletter/NewsletterManager")
|
||||
async = require("async")
|
||||
|
@ -47,7 +48,7 @@ module.exports = UserRegistrationHandler =
|
|||
if !requestIsValid
|
||||
return callback(new Error("request is not valid"))
|
||||
userDetails.email = userDetails.email?.trim()?.toLowerCase()
|
||||
User.findOne email:userDetails.email, (err, user)->
|
||||
UserGetter.getUserByMainEmail userDetails.email, (err, user) =>
|
||||
if err?
|
||||
return callback err
|
||||
if user?.holdingAccount == false
|
||||
|
|
|
@ -3,7 +3,7 @@ mongojs = require("../../infrastructure/mongojs")
|
|||
metrics = require("metrics-sharelatex")
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
UserLocator = require("./UserLocator")
|
||||
UserGetter = require("./UserGetter")
|
||||
|
||||
module.exports = UserUpdater =
|
||||
updateUser: (query, update, callback = (error) ->) ->
|
||||
|
@ -18,7 +18,7 @@ module.exports = UserUpdater =
|
|||
changeEmailAddress: (user_id, newEmail, callback)->
|
||||
self = @
|
||||
logger.log user_id:user_id, newEmail:newEmail, "updaing email address of user"
|
||||
UserLocator.findByEmail newEmail, (error, user) ->
|
||||
UserGetter.getUserByMainEmail newEmail, (error, user) ->
|
||||
if user?
|
||||
return callback({message:"alread_exists"})
|
||||
self.updateUser user_id.toString(), {
|
||||
|
|
|
@ -6,16 +6,31 @@ Settings = require 'settings-sharelatex'
|
|||
request = require 'request'
|
||||
|
||||
module.exports = FileWriter =
|
||||
|
||||
_ensureDumpFolderExists: (callback=(error)->) ->
|
||||
fs.mkdir Settings.path.dumpFolder, (error) ->
|
||||
if error? and error.code != 'EEXIST'
|
||||
# Ignore error about already existing
|
||||
return callback(error)
|
||||
callback(null)
|
||||
|
||||
writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) ->
|
||||
callback = _.once(callback)
|
||||
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
|
||||
FileWriter._ensureDumpFolderExists (error) ->
|
||||
return callback(error) if error?
|
||||
fs.writeFile fsPath, lines.join('\n'), (error) ->
|
||||
return callback(error) if error?
|
||||
callback(null, fsPath)
|
||||
|
||||
writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
|
||||
|
||||
stream.pause()
|
||||
fs.mkdir Settings.path.dumpFolder, (error) ->
|
||||
FileWriter._ensureDumpFolderExists (error) ->
|
||||
return callback(error) if error?
|
||||
stream.resume()
|
||||
if error? and error.code != 'EEXIST'
|
||||
# Ignore error about already existing
|
||||
return callback(error)
|
||||
|
||||
writeStream = fs.createWriteStream(fsPath)
|
||||
stream.pipe(writeStream)
|
||||
|
@ -39,4 +54,4 @@ module.exports = FileWriter =
|
|||
else
|
||||
err = new Error("bad response from url: #{response.statusCode}")
|
||||
logger.err {err, identifier, url}, err.message
|
||||
callback(err)
|
||||
callback(err)
|
||||
|
|
|
@ -30,7 +30,8 @@ module.exports = Modules =
|
|||
for module in @modules
|
||||
for view, partial of module.viewIncludes or {}
|
||||
@viewIncludes[view] ||= []
|
||||
@viewIncludes[view].push pug.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug")), doctype: "html")
|
||||
filePath = Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug")
|
||||
@viewIncludes[view].push pug.compileFile(filePath, doctype: "html")
|
||||
|
||||
moduleIncludes: (view, locals) ->
|
||||
compiledPartials = Modules.viewIncludes[view] or []
|
||||
|
|
|
@ -48,6 +48,7 @@ MetaController = require('./Features/Metadata/MetaController')
|
|||
TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
|
||||
Features = require('./infrastructure/Features')
|
||||
LinkedFilesRouter = require './Features/LinkedFiles/LinkedFilesRouter'
|
||||
TemplatesRouter = require './Features/Templates/TemplatesRouter'
|
||||
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
|
@ -80,10 +81,10 @@ module.exports = class Router
|
|||
ContactRouter.apply(webRouter, privateApiRouter)
|
||||
AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter)
|
||||
LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter)
|
||||
TemplatesRouter.apply(webRouter)
|
||||
|
||||
Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
|
||||
|
||||
|
||||
if Settings.enableSubscriptions
|
||||
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus
|
||||
|
||||
|
@ -119,6 +120,11 @@ module.exports = class Router
|
|||
webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
|
||||
privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
|
||||
|
||||
webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.userProjectsJson
|
||||
webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(),
|
||||
AuthorizationMiddlewear.ensureUserCanReadProject,
|
||||
ProjectController.projectEntitiesJson
|
||||
|
||||
webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage
|
||||
webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
|
||||
|
||||
|
@ -201,7 +207,7 @@ module.exports = class Router
|
|||
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
|
||||
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.proxyToHistoryApiAndInjectUserDetails
|
||||
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
|
||||
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
|
||||
webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
|
||||
webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
|
||||
privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
script(type='text/ng-template', id='supportModalTemplate')
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
ng-click="close()"
|
||||
) ×
|
||||
h3 #{translate("contact_us")}
|
||||
.modal-body.contact-us-modal
|
||||
form(name="contactForm")
|
||||
span(ng-show="sent == false")
|
||||
.alert.alert-danger(ng-show="error") Something went wrong sending your request :(
|
||||
label
|
||||
| #{translate("subject")}
|
||||
.form-group
|
||||
input.field.text.medium.span8.form-control(
|
||||
name="subject",
|
||||
required
|
||||
ng-model="form.subject",
|
||||
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }"
|
||||
maxlength='255',
|
||||
tabindex='1',
|
||||
onkeyup='')
|
||||
.contact-suggestions(ng-show="suggestions.length")
|
||||
p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>" + translate("knowledge_base") + "</a>" })}
|
||||
ul.contact-suggestion-list
|
||||
li(ng-repeat="suggestion in suggestions")
|
||||
a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
|
||||
span(ng-bind-html="suggestion.name")
|
||||
i.fa.fa-angle-right
|
||||
label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
|
||||
| #{translate("email")}
|
||||
.form-group(ng-show="'"+getUserEmail()+"'.length < 1")
|
||||
input.field.text.medium.span8.form-control(
|
||||
name="email",
|
||||
required
|
||||
ng-model="form.email",
|
||||
ng-init="form.email = '"+getUserEmail()+"'",
|
||||
type='email', spellcheck='false',
|
||||
value='',
|
||||
maxlength='255',
|
||||
tabindex='2')
|
||||
label#title12.desc
|
||||
| #{translate("project_url")} (#{translate("optional")})
|
||||
.form-group
|
||||
input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='')
|
||||
label.desc
|
||||
| #{translate("contact_message_label")}
|
||||
.form-group
|
||||
textarea.field.text.medium.span8.form-control(
|
||||
name="body",
|
||||
required
|
||||
ng-model="form.message",
|
||||
type='text',
|
||||
value='',
|
||||
tabindex='4',
|
||||
onkeyup=''
|
||||
)
|
||||
.form-group.text-center
|
||||
input.btn-success.btn.btn-lg(
|
||||
type='submit',
|
||||
ng-disabled="contactForm.$invalid || sending",
|
||||
ng-click="contactUs()"
|
||||
value=translate("contact_us")
|
||||
)
|
||||
span(ng-show="sent")
|
||||
p #{translate("request_sent_thank_you")}
|
|
@ -147,7 +147,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
src=buildJsPath('libs/require.js', {hashedPath:true})
|
||||
)
|
||||
|
||||
include contact-us-modal
|
||||
!= moduleIncludes("contactModal", locals)
|
||||
include v1-tooltip
|
||||
include sentry
|
||||
|
||||
|
|
|
@ -47,7 +47,18 @@ div.binary-file.full-size(
|
|||
|
|
||||
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
|
||||
|
||||
span(ng-if="openFile.linkedFileData.provider == 'url'")
|
||||
div(ng-if="openFile.linkedFileData.provider == 'project_file'")
|
||||
p
|
||||
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
|
||||
| Imported from
|
||||
|
|
||||
a(ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank")
|
||||
| {{ openFile.linkedFileData.source_project_display_name }}
|
||||
| /{{ openFile.linkedFileData.source_entity_path.slice(1) }},
|
||||
|
|
||||
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
|
||||
|
||||
span(ng-if="openFile.linkedFileData.provider == 'url' || openFile.linkedFileData.provider == 'project_file'")
|
||||
button.btn.btn-success(
|
||||
href, ng-click="refreshFile(openFile)",
|
||||
ng-disabled="refreshing"
|
||||
|
@ -63,3 +74,7 @@ div.binary-file.full-size(
|
|||
i.fa.fa-fw.fa-download
|
||||
|
|
||||
| #{translate("download")}
|
||||
div(ng-if="refreshError")
|
||||
br
|
||||
.alert.alert-danger.col-md-6.col-md-offset-3
|
||||
| Error: {{ refreshError}}
|
||||
|
|
|
@ -33,11 +33,11 @@ div.full-size(
|
|||
i.fa.fa-arrow-left
|
||||
| #{translate("open_a_file_on_the_left")}
|
||||
|
||||
!= moduleIncludes('editor:toolbar', locals)
|
||||
!= moduleIncludes('editor:main', locals)
|
||||
|
||||
#editor(
|
||||
ace-editor="editor",
|
||||
ng-if="!editor.richText",
|
||||
ng-if="!editor.showRichText",
|
||||
ng-show="!!editor.sharejs_doc && !editor.opening",
|
||||
style=showRichText ? "top: 32px" : "",
|
||||
theme="settings.theme",
|
||||
|
@ -73,8 +73,6 @@ div.full-size(
|
|||
line-height="settings.lineHeight || ui.defaultLineHeight"
|
||||
)
|
||||
|
||||
!= moduleIncludes('editor:body', locals)
|
||||
|
||||
include ./review-panel
|
||||
|
||||
.ui-layout-east
|
||||
|
|
|
@ -342,6 +342,77 @@ script(type='text/ng-template', id='newDocModalTemplate')
|
|||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
// Project Linked Files Modal
|
||||
script(type='text/ng-template', id='projectLinkedFileModalTemplate')
|
||||
.modal-header
|
||||
h3 New file from Project
|
||||
|
||||
.modal-body
|
||||
div
|
||||
div.alert.alert-danger(ng-if="state.error") Error, something went wrong!
|
||||
div
|
||||
form
|
||||
.form-controls
|
||||
label(for="project-select") Select a Project
|
||||
span(ng-show="state.inFlight.projects")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-select"
|
||||
ng-model="data.selectedProjectId"
|
||||
ng-disabled="!shouldEnableProjectSelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a Project
|
||||
option(
|
||||
ng-repeat="project in data.projects"
|
||||
value="{{ project._id }}"
|
||||
) {{ project.name }}
|
||||
|
||||
br
|
||||
.form-controls
|
||||
label(for="project-entity-select") Select a File
|
||||
span(ng-show="state.inFlight.entities")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-entity-select"
|
||||
ng-model="data.selectedProjectEntity"
|
||||
ng-disabled="!shouldEnableProjectEntitySelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a File
|
||||
option(
|
||||
ng-repeat="projectEntity in data.projectEntities"
|
||||
value="{{ projectEntity.path }}"
|
||||
) {{ projectEntity.path.slice(1) }}
|
||||
br
|
||||
|
||||
.form-controls
|
||||
label(for="name") File Name In This Project
|
||||
input.form-control(
|
||||
type="text"
|
||||
placeholder="example.tex"
|
||||
required
|
||||
ng-model="data.name"
|
||||
name="name"
|
||||
)
|
||||
br
|
||||
|
||||
.modal-footer
|
||||
span(ng-show="state.inFlight.create")
|
||||
i.fa.fa-spinner.fa-spin
|
||||
|
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="cancel()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
ng-disabled="!shouldEnableCreateButton()"
|
||||
ng-click="create()"
|
||||
)
|
||||
span(ng-hide="state.inflight") #{translate("create")}
|
||||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
script(type='text/ng-template', id='linkedFileModalTemplate')
|
||||
.modal-header
|
||||
h3 New file from URL
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
}"
|
||||
)
|
||||
| in <strong>{{history.diff.pathname}}</strong>
|
||||
.toolbar-right
|
||||
.toolbar-right(ng-if="permissions.write")
|
||||
a.btn.btn-danger.btn-sm(
|
||||
href,
|
||||
ng-click="openRestoreDiffModal()"
|
||||
|
|
|
@ -62,6 +62,23 @@ aside#left-menu.full-size(
|
|||
!= moduleIncludes("editorLeftMenu:editing_services", locals)
|
||||
|
||||
|
||||
if showTestControls
|
||||
h4 Test Controls
|
||||
ul.list-unstyled.nav(ng-controller="TestControlsController")
|
||||
li
|
||||
a(href="#" ng-click="richText()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| Rich Text
|
||||
li
|
||||
a(href="#" ng-click="openProjectLinkedFileModal()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| Project-Linked-File Modal
|
||||
li
|
||||
a(href="#" ng-click="openLinkedFileModal()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| URL-Linked-File Modal
|
||||
|
||||
|
||||
h4(ng-show="!anonymous") #{translate("settings")}
|
||||
form.settings(ng-controller="SettingsController", ng-show="!anonymous")
|
||||
.containter-fluid
|
||||
|
@ -179,6 +196,7 @@ aside#left-menu.full-size(
|
|||
option(value="pdfjs") #{translate("built_in")}
|
||||
option(value="native") #{translate("native")}
|
||||
|
||||
|
||||
h4 #{translate("hotkeys")}
|
||||
ul.list-unstyled.nav
|
||||
li(ng-controller="HotkeysController")
|
||||
|
|
26
services/web/app/views/project/editor/new_from_template.pug
Normal file
26
services/web/app/views/project/editor/new_from_template.pug
Normal file
|
@ -0,0 +1,26 @@
|
|||
extends ../../layout
|
||||
|
||||
block content
|
||||
script.
|
||||
$(document).ready(function(){
|
||||
$('#create_form').submit();
|
||||
});
|
||||
|
||||
.editor.full-size
|
||||
.loading-screen()
|
||||
.loading-screen-brand-container
|
||||
.loading-screen-brand(
|
||||
style="height: 20%;"
|
||||
)
|
||||
|
||||
h3.loading-screen-label() #{translate("Opening template")}
|
||||
span.loading-screen-ellip .
|
||||
span.loading-screen-ellip .
|
||||
span.loading-screen-ellip .
|
||||
|
||||
form(id='create_form' method='POST' action='/project/new/template/')
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
input(type="hidden" name="templateId" value=templateId)
|
||||
input(type="hidden" name="templateVersionId" value=templateVersionId)
|
||||
input(type="hidden" name="templateName" value=name)
|
||||
input(type="hidden" name="compiler" value=compiler)
|
|
@ -1,4 +1,7 @@
|
|||
.col-xs-6
|
||||
- var titleClasses = settings.overleaf ? "col-xs-6 col-sm-4 col-md-6" : "col-xs-6"
|
||||
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4"
|
||||
|
||||
div(class=titleClasses)
|
||||
input.select-item(
|
||||
select-individual,
|
||||
type="checkbox",
|
||||
|
@ -37,8 +40,42 @@
|
|||
tooltip-placement="right"
|
||||
tooltip-append-to-body="true"
|
||||
)
|
||||
.col-xs-4
|
||||
|
||||
div(class=lastUpdatedClasses)
|
||||
if settings.overleaf
|
||||
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}
|
||||
else
|
||||
span.last-modified {{project.lastUpdated | formatDate}}
|
||||
|
||||
if settings.overleaf
|
||||
.hidden-xs.col-sm-3.col-md-2.action-btn-row
|
||||
button.btn.btn-link.action-btn(
|
||||
tooltip=translate('copy'),
|
||||
tooltip-placement="top",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="clone($event)"
|
||||
)
|
||||
i.icon.fa.fa-files-o
|
||||
button.btn.btn-link.action-btn(
|
||||
tooltip=translate('download'),
|
||||
tooltip-placement="top",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="download($event)"
|
||||
)
|
||||
i.icon.fa.fa-cloud-download
|
||||
button.btn.btn-link.action-btn(
|
||||
ng-if="!project.archived"
|
||||
tooltip=translate('archive'),
|
||||
tooltip-placement="top",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="archive($event)"
|
||||
)
|
||||
i.icon.fa.fa-inbox
|
||||
button.btn.btn-link.action-btn(
|
||||
ng-if="project.archived"
|
||||
tooltip=translate('unarchive'),
|
||||
tooltip-placement="top",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="restore($event)"
|
||||
)
|
||||
i.icon.fa.fa-reply
|
|
@ -131,7 +131,10 @@
|
|||
)
|
||||
li.container-fluid
|
||||
.row
|
||||
.col-xs-6
|
||||
- var titleClasses = settings.overleaf ? " col-xs-6 col-sm-4 col-md-6" : "col-xs-6"
|
||||
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4"
|
||||
|
||||
div(class=titleClasses)
|
||||
input.select-all(
|
||||
select-all,
|
||||
type="checkbox"
|
||||
|
@ -142,9 +145,12 @@
|
|||
.col-xs-2
|
||||
span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")}
|
||||
i.tablesort.fa(ng-class="getSortIconClass('accessLevel')")
|
||||
.col-xs-4
|
||||
div(class=lastUpdatedClasses)
|
||||
span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")}
|
||||
i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')")
|
||||
if settings.overleaf
|
||||
.hidden-xs.col-sm-3.col-md-2.action-btn-row-header
|
||||
span.header #{translate("actions")}
|
||||
li.project_entry.container-fluid(
|
||||
ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
|
||||
ng-controller="ProjectListItemController"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.col-xs-6
|
||||
.col-xs-6.col-sm-4.col-md-6
|
||||
.select-item
|
||||
span.v1-badge(
|
||||
aria-label=translate("v1_badge")
|
||||
|
@ -21,5 +21,5 @@
|
|||
.col-xs-2
|
||||
span.owner {{ownerName()}}
|
||||
|
||||
.col-xs-4
|
||||
.col-xs-4.col-sm-3.col-md-2
|
||||
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}
|
|
@ -146,8 +146,8 @@ module.exports = settings =
|
|||
url: "http://#{process.env['CONTACTS_HOST'] or 'localhost'}:3036"
|
||||
sixpack:
|
||||
url: ""
|
||||
# references:
|
||||
# url: "http://localhost:3040"
|
||||
references:
|
||||
url: "http://#{process.env['REFERENCES_HOST'] or 'localhost'}:3040"
|
||||
notifications:
|
||||
url: "http://#{process.env['NOTIFICATIONS_HOST'] or 'localhost'}:3042"
|
||||
analytics:
|
||||
|
|
|
@ -17,6 +17,7 @@ services:
|
|||
PROJECT_HISTORY_ENABLED: 'true'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url'
|
||||
LINKED_URL_PROXY: 'http://localhost:6543'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url,project_file'
|
||||
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
|
||||
depends_on:
|
||||
- redis
|
||||
|
|
|
@ -18,6 +18,7 @@ define [
|
|||
"ide/chat/index"
|
||||
"ide/clone/index"
|
||||
"ide/hotkeys/index"
|
||||
"ide/test-controls/index"
|
||||
"ide/wordcount/index"
|
||||
"ide/directives/layout"
|
||||
"ide/directives/validFile"
|
||||
|
@ -34,6 +35,7 @@ define [
|
|||
"directives/videoPlayState"
|
||||
"services/queued-http"
|
||||
"services/validateCaptcha"
|
||||
"services/wait-for"
|
||||
"filters/formatDate"
|
||||
"main/event"
|
||||
"main/account-upgrade"
|
||||
|
@ -54,7 +56,7 @@ define [
|
|||
SafariScrollPatcher
|
||||
) ->
|
||||
|
||||
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata) ->
|
||||
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata, $q) ->
|
||||
# Don't freak out if we're already in an apply callback
|
||||
$scope.$originalApply = $scope.$apply
|
||||
$scope.$apply = (fn = () ->) ->
|
||||
|
|
|
@ -2,7 +2,7 @@ define [
|
|||
"base"
|
||||
"moment"
|
||||
], (App, moment) ->
|
||||
App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", ($scope, $rootScope, $http, $timeout, $element, ide) ->
|
||||
App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", "waitFor", ($scope, $rootScope, $http, $timeout, $element, ide, waitFor) ->
|
||||
|
||||
TWO_MEGABYTES = 2 * 1024 * 1024
|
||||
|
||||
|
@ -31,6 +31,7 @@ define [
|
|||
data: null
|
||||
|
||||
$scope.refreshing = false
|
||||
$scope.refreshError = null
|
||||
|
||||
MAX_URL_LENGTH = 60
|
||||
FRONT_OF_URL_LENGTH = 35
|
||||
|
@ -48,9 +49,27 @@ define [
|
|||
|
||||
$scope.refreshFile = (file) ->
|
||||
$scope.refreshing = true
|
||||
$scope.refreshError = null
|
||||
ide.fileTreeManager.refreshLinkedFile(file)
|
||||
.then () ->
|
||||
loadTextFileFilePreview()
|
||||
.then (response) ->
|
||||
{ data } = response
|
||||
{ new_file_id } = data
|
||||
$timeout(
|
||||
() ->
|
||||
waitFor(
|
||||
() ->
|
||||
ide.fileTreeManager.findEntityById(new_file_id)
|
||||
5000
|
||||
)
|
||||
.then (newFile) ->
|
||||
ide.binaryFilesManager.openFile(newFile)
|
||||
.catch (err) ->
|
||||
console.warn(err)
|
||||
, 0
|
||||
)
|
||||
$scope.refreshError = null
|
||||
.catch (response) ->
|
||||
$scope.refreshError = response.data
|
||||
.finally () ->
|
||||
$scope.refreshing = false
|
||||
|
||||
|
@ -86,11 +105,9 @@ define [
|
|||
# show dots when payload is closs to cutoff
|
||||
if data.length >= (TWO_MEGABYTES - 200)
|
||||
$scope.textPreview.shouldShowDots = true
|
||||
try
|
||||
# remove last partial line
|
||||
data = data.replace(/\n.*$/, '')
|
||||
finally
|
||||
$scope.textPreview.data = data
|
||||
data = data?.replace?(/\n.*$/, '')
|
||||
$scope.textPreview.data = data
|
||||
$timeout(setHeight, 0)
|
||||
.catch (error) ->
|
||||
console.error(error)
|
||||
|
|
|
@ -14,7 +14,7 @@ define [
|
|||
opening: true
|
||||
trackChanges: false
|
||||
wantTrackChanges: false
|
||||
richText: false
|
||||
showRichText: false
|
||||
}
|
||||
|
||||
@$scope.$on "entity:selected", (event, entity) =>
|
||||
|
|
|
@ -43,6 +43,19 @@ define [
|
|||
}
|
||||
)
|
||||
|
||||
$scope.openProjectLinkedFileModal = window.openProjectLinkedFileModal = () ->
|
||||
unless 'project_file' in window.data.enabledLinkedFileTypes
|
||||
console.warn("Project linked files are not enabled")
|
||||
return
|
||||
$modal.open(
|
||||
templateUrl: "projectLinkedFileModalTemplate"
|
||||
controller: "ProjectLinkedFileModalController"
|
||||
scope: $scope
|
||||
resolve: {
|
||||
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
|
||||
}
|
||||
)
|
||||
|
||||
$scope.orderByFoldersFirst = (entity) ->
|
||||
return '0' if entity?.type == "folder"
|
||||
return '1'
|
||||
|
@ -201,6 +214,117 @@ define [
|
|||
$modalInstance.dismiss('cancel')
|
||||
]
|
||||
|
||||
App.controller "ProjectLinkedFileModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
$scope.data =
|
||||
projects: null # or []
|
||||
selectedProjectId: null
|
||||
projectEntities: null # or []
|
||||
selectedProjectEntity: null
|
||||
name: null
|
||||
$scope.state =
|
||||
inFlight:
|
||||
projects: false
|
||||
entities: false
|
||||
create: false
|
||||
error: false
|
||||
|
||||
$scope.$watch 'data.selectedProjectId', (newVal, oldVal) ->
|
||||
return if !newVal
|
||||
$scope.data.selectedProjectEntity = null
|
||||
$scope.getProjectEntities($scope.data.selectedProjectId)
|
||||
|
||||
# auto-set filename based on selected file
|
||||
$scope.$watch 'data.selectedProjectEntity', (newVal, oldVal) ->
|
||||
return if !newVal
|
||||
fileName = newVal.split('/').reverse()[0]
|
||||
if fileName
|
||||
$scope.data.name = fileName
|
||||
|
||||
_setInFlight = (type) ->
|
||||
$scope.state.inFlight[type] = true
|
||||
|
||||
_reset = (opts) ->
|
||||
isError = opts.err == true
|
||||
inFlight = $scope.state.inFlight
|
||||
inFlight.projects = inFlight.entities = inFlight.create = false
|
||||
$scope.state.error = isError
|
||||
|
||||
$scope.shouldEnableProjectSelect = () ->
|
||||
{ state, data } = $scope
|
||||
return !state.inFlight.projects && data.projects
|
||||
|
||||
$scope.shouldEnableProjectEntitySelect = () ->
|
||||
{ state, data } = $scope
|
||||
return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId
|
||||
|
||||
$scope.shouldEnableCreateButton = () ->
|
||||
state = $scope.state
|
||||
data = $scope.data
|
||||
return !state.inFlight.projects &&
|
||||
!state.inFlight.entities &&
|
||||
data.projects &&
|
||||
data.selectedProjectId &&
|
||||
data.projectEntities &&
|
||||
data.selectedProjectEntity &&
|
||||
data.name
|
||||
|
||||
$scope.getUserProjects = () ->
|
||||
_setInFlight('projects')
|
||||
ide.$http.get("/user/projects", {
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
.then (resp) ->
|
||||
$scope.data.projectEntities = null
|
||||
$scope.data.projects = resp.data.projects.filter (p) ->
|
||||
p._id != ide.project_id
|
||||
_reset(err: false)
|
||||
.catch (err) ->
|
||||
_reset(err: true)
|
||||
|
||||
$scope.getProjectEntities = (project_id) =>
|
||||
_setInFlight('entities')
|
||||
ide.$http.get("/project/#{project_id}/entities", {
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
.then (resp) ->
|
||||
if $scope.data.selectedProjectId == resp.data.project_id
|
||||
$scope.data.projectEntities = resp.data.entities
|
||||
_reset(err: false)
|
||||
.catch (err) ->
|
||||
_reset(err: true)
|
||||
|
||||
$scope.init = () ->
|
||||
$scope.getUserProjects()
|
||||
$timeout($scope.init, 0)
|
||||
|
||||
$scope.create = () ->
|
||||
projectId = $scope.data.selectedProjectId
|
||||
path = $scope.data.selectedProjectEntity
|
||||
name = $scope.data.name
|
||||
if !name || !path || !projectId
|
||||
_reset(err: true)
|
||||
return
|
||||
_setInFlight('create')
|
||||
ide.fileTreeManager
|
||||
.createLinkedFile(name, parent_folder, 'project_file', {
|
||||
source_project_id: projectId,
|
||||
source_entity_path: path
|
||||
})
|
||||
.then () ->
|
||||
_reset(err: false)
|
||||
$modalInstance.close()
|
||||
.catch (response)->
|
||||
{ data } = response
|
||||
_reset(err: true)
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
|
||||
]
|
||||
|
||||
# TODO: rename all this to UrlLinkedFilModalController
|
||||
App.controller "LinkedFileModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking) ->
|
||||
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking, waitFor) ->
|
||||
$scope.restoreState =
|
||||
inflight: false
|
||||
error: false
|
||||
|
@ -24,17 +24,16 @@ define [
|
|||
$scope.restoreState.inflight = false
|
||||
|
||||
openEntity = (data) ->
|
||||
iterations = 0
|
||||
{id, type} = data
|
||||
do tryOpen = () ->
|
||||
if iterations > 5
|
||||
return
|
||||
iterations += 1
|
||||
entity = ide.fileTreeManager.findEntityById(id)
|
||||
if entity? and type == 'doc'
|
||||
ide.editorManager.openDoc(entity)
|
||||
else if entity? and type == 'file'
|
||||
ide.binaryFilesManager.openFile(entity)
|
||||
else
|
||||
setTimeout(tryOpen, 500)
|
||||
|
||||
waitFor(
|
||||
() ->
|
||||
ide.fileTreeManager.findEntityById(id)
|
||||
3000
|
||||
)
|
||||
.then (entity) ->
|
||||
if type == 'doc'
|
||||
ide.editorManager.openDoc(entity)
|
||||
else if type == 'file'
|
||||
ide.binaryFilesManager.openFile(entity)
|
||||
.catch (err) ->
|
||||
console.warn(err)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
define [
|
||||
"base"
|
||||
"ace/ace"
|
||||
], (App) ->
|
||||
App.controller "TestControlsController", ($scope) ->
|
||||
|
||||
$scope.openProjectLinkedFileModal = () ->
|
||||
window.openProjectLinkedFileModal()
|
||||
|
||||
$scope.openLinkedFileModal = () ->
|
||||
window.openLinkedFileModal()
|
||||
|
||||
$scope.richText = () ->
|
||||
current = window.location.toString()
|
||||
target = "#{current}#{if window.location.search then '&' else '?'}rt=true"
|
||||
window.location.href = target
|
|
@ -0,0 +1,3 @@
|
|||
define [
|
||||
"ide/test-controls/controllers/TestControlsController"
|
||||
], () ->
|
|
@ -1,79 +1,7 @@
|
|||
define [
|
||||
"base"
|
||||
"libs/platform"
|
||||
"services/algolia-search"
|
||||
], (App, platform) ->
|
||||
App.controller 'ContactModal', ($scope, $modal) ->
|
||||
$scope.contactUsModal = () ->
|
||||
modalInstance = $modal.open(
|
||||
templateUrl: "supportModalTemplate"
|
||||
controller: "SupportModalController"
|
||||
)
|
||||
|
||||
App.controller 'SupportModalController', ($scope, $modalInstance, algoliaSearch, event_tracking) ->
|
||||
$scope.form = {}
|
||||
$scope.sent = false
|
||||
$scope.sending = false
|
||||
$scope.suggestions = [];
|
||||
|
||||
_handleSearchResults = (success, results) ->
|
||||
suggestions = for hit in results.hits
|
||||
page_underscored = hit.pageName.replace(/\s/g,'_')
|
||||
|
||||
suggestion =
|
||||
url :"/learn/kb/#{page_underscored}"
|
||||
name : hit._highlightResult.pageName.value
|
||||
|
||||
event_tracking.sendMB "contact-form-suggestions-shown" if results.hits.length
|
||||
|
||||
$scope.$applyAsync () ->
|
||||
$scope.suggestions = suggestions
|
||||
|
||||
$scope.contactUs = ->
|
||||
if !$scope.form.email? or $scope.form.email == ""
|
||||
console.log "email not set"
|
||||
return
|
||||
$scope.sending = true
|
||||
ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32)
|
||||
message = $scope.form.message
|
||||
if $scope.form.project_url?
|
||||
message = "#{message}\n\n project_url = #{$scope.form.project_url}"
|
||||
params =
|
||||
email: $scope.form.email
|
||||
message: message or ""
|
||||
subject: $scope.form.subject + " - [#{ticketNumber}]"
|
||||
labels: "support"
|
||||
about: "<div>browser: #{platform?.name} #{platform?.version}</div>
|
||||
<div>os: #{platform?.os?.family} #{platform?.os?.version}</div>"
|
||||
|
||||
Groove.createTicket params, (response)->
|
||||
$scope.sending = false
|
||||
if response.responseText == "" # Blocked request or similar
|
||||
$scope.error = true
|
||||
else
|
||||
data = JSON.parse(response.responseText)
|
||||
if data.errors?
|
||||
$scope.error = true
|
||||
else
|
||||
$scope.sent = true
|
||||
$scope.$apply()
|
||||
|
||||
$scope.$watch "form.subject", (newVal, oldVal) ->
|
||||
if newVal and newVal != oldVal and newVal.length > 3
|
||||
algoliaSearch.searchKB newVal, _handleSearchResults, {
|
||||
hitsPerPage: 3
|
||||
typoTolerance: 'strict'
|
||||
}
|
||||
else
|
||||
$scope.suggestions = [];
|
||||
|
||||
$scope.clickSuggestionLink = (url) ->
|
||||
event_tracking.sendMB "contact-form-suggestions-clicked", { url }
|
||||
|
||||
$scope.close = () ->
|
||||
$modalInstance.close()
|
||||
|
||||
|
||||
App.controller 'UniverstiesContactController', ($scope, $modal, $http) ->
|
||||
|
||||
$scope.form = {}
|
||||
|
|
|
@ -350,14 +350,15 @@ define [
|
|||
$scope.archiveOrLeaveSelectedProjects()
|
||||
|
||||
$scope.archiveOrLeaveSelectedProjects = () ->
|
||||
selected_projects = $scope.getSelectedProjects()
|
||||
selected_project_ids = $scope.getSelectedProjectIds()
|
||||
$scope.archiveOrLeaveProjects($scope.getSelectedProjects())
|
||||
|
||||
$scope.archiveOrLeaveProjects = (projects) ->
|
||||
projectIds = projects.map (p) -> p.id
|
||||
# Remove project from any tags
|
||||
for tag in $scope.tags
|
||||
$scope._removeProjectIdsFromTagArray(tag, selected_project_ids)
|
||||
$scope._removeProjectIdsFromTagArray(tag, projectIds)
|
||||
|
||||
for project in selected_projects
|
||||
for project in projects
|
||||
project.tags = []
|
||||
if project.accessLevel == "owner"
|
||||
project.archived = true
|
||||
|
@ -414,13 +415,14 @@ define [
|
|||
$scope.updateVisibleProjects()
|
||||
|
||||
$scope.restoreSelectedProjects = () ->
|
||||
selected_projects = $scope.getSelectedProjects()
|
||||
selected_project_ids = $scope.getSelectedProjectIds()
|
||||
$scope.restoreProjects($scope.getSelectedProjects())
|
||||
|
||||
for project in selected_projects
|
||||
$scope.restoreProjects = (projects) ->
|
||||
projectIds = projects.map (p) -> p.id
|
||||
for project in projects
|
||||
project.archived = false
|
||||
|
||||
for project_id in selected_project_ids
|
||||
for projectId in projectIds
|
||||
queuedHttp {
|
||||
method: "POST"
|
||||
url: "/project/#{project_id}/restore"
|
||||
|
@ -437,13 +439,14 @@ define [
|
|||
)
|
||||
|
||||
$scope.downloadSelectedProjects = () ->
|
||||
selected_project_ids = $scope.getSelectedProjectIds()
|
||||
event_tracking.send 'project-list-page-interaction', 'project action', 'Download Zip'
|
||||
if selected_project_ids.length > 1
|
||||
path = "/project/download/zip?project_ids=#{selected_project_ids.join(',')}"
|
||||
else
|
||||
path = "/project/#{selected_project_ids[0]}/download/zip"
|
||||
$scope.downloadProjectsById($scope.getSelectedProjectIds())
|
||||
|
||||
$scope.downloadProjectsById = (projectIds) ->
|
||||
event_tracking.send 'project-list-page-interaction', 'project action', 'Download Zip'
|
||||
if projectIds.length > 1
|
||||
path = "/project/download/zip?project_ids=#{projectIds.join(',')}"
|
||||
else
|
||||
path = "/project/#{projectIds[0]}/download/zip"
|
||||
window.location = path
|
||||
|
||||
$scope.openV1ImportModal = (project) ->
|
||||
|
@ -490,3 +493,19 @@ define [
|
|||
$scope.$watch "project.selected", (value) ->
|
||||
if value?
|
||||
$scope.updateSelectedProjects()
|
||||
|
||||
$scope.clone = (e) ->
|
||||
e.stopPropagation()
|
||||
$scope.cloneProject($scope.project, "#{$scope.project.name} (Copy)")
|
||||
|
||||
$scope.download = (e) ->
|
||||
e.stopPropagation()
|
||||
$scope.downloadProjectsById([$scope.project.id])
|
||||
|
||||
$scope.archive = (e) ->
|
||||
e.stopPropagation()
|
||||
$scope.archiveOrLeaveProjects([$scope.project])
|
||||
|
||||
$scope.restore = (e) ->
|
||||
e.stopPropagation()
|
||||
$scope.restoreProjects([$scope.project])
|
||||
|
|
|
@ -8,7 +8,7 @@ define [
|
|||
kbIdx = client.initIndex(window.sharelatex.algolia?.indexes?.kb)
|
||||
|
||||
service =
|
||||
searchWiki: wikiIdx.search.bind(wikiIdx)
|
||||
searchKB: kbIdx.search.bind(kbIdx)
|
||||
searchWiki: if wikiIdx then wikiIdx.search.bind(wikiIdx) else null
|
||||
searchKB: if kbIdx then kbIdx.search.bind(kbIdx) else null
|
||||
|
||||
return service
|
20
services/web/public/coffee/services/wait-for.coffee
Normal file
20
services/web/public/coffee/services/wait-for.coffee
Normal file
|
@ -0,0 +1,20 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.factory "waitFor", ($q) ->
|
||||
waitFor = (testFunction, timeout, pollInterval=500) ->
|
||||
iterationLimit = Math.floor(timeout / pollInterval)
|
||||
iterations = 0
|
||||
$q(
|
||||
(resolve, reject) ->
|
||||
do tryIteration = () ->
|
||||
if iterations > iterationLimit
|
||||
return reject(new Error("waiting too long, #{JSON.stringify({timeout, pollInterval})}"))
|
||||
iterations += 1
|
||||
result = testFunction()
|
||||
if result?
|
||||
resolve(result)
|
||||
else
|
||||
setTimeout(tryIteration, pollInterval)
|
||||
)
|
||||
return waitFor
|
|
@ -369,6 +369,16 @@ ul.project-list {
|
|||
.v1-badge {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.action-btn-row-header, .action-btn-row {
|
||||
padding-right: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0 0.3em;
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
}
|
||||
i.tablesort {
|
||||
padding-left: 8px;
|
||||
|
|
|
@ -145,10 +145,15 @@ output {
|
|||
opacity: 1; // iOS fix for unreadable disabled content
|
||||
}
|
||||
|
||||
// Reset height for `textarea`s
|
||||
// Reset height for `textarea`s, and smaller border-radius
|
||||
textarea& {
|
||||
height: auto;
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
// Smaller border-radius for `select` inputs
|
||||
select& {
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -27,6 +27,122 @@ describe "LinkedFiles", ->
|
|||
@owner.login ->
|
||||
mkdirp Settings.path.dumpFolder, done
|
||||
|
||||
describe "creating a project linked file", ->
|
||||
before (done) ->
|
||||
@source_doc_name = 'test.txt'
|
||||
async.series [
|
||||
(cb) =>
|
||||
@owner.createProject 'plf-test-one', {template: 'blank'}, (error, project_id) =>
|
||||
@project_one_id = project_id
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.getProject @project_one_id, (error, project) =>
|
||||
@project_one = project
|
||||
@project_one_root_folder_id = project.rootFolder[0]._id.toString()
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.createProject 'plf-test-two', {template: 'blank'}, (error, project_id) =>
|
||||
@project_two_id = project_id
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.getProject @project_two_id, (error, project) =>
|
||||
@project_two = project
|
||||
@project_two_root_folder_id = project.rootFolder[0]._id.toString()
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.createDocInProject @project_two_id,
|
||||
@project_two_root_folder_id,
|
||||
@source_doc_name,
|
||||
(error, doc_id) =>
|
||||
@source_doc_id = doc_id
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.createDocInProject @project_two_id,
|
||||
@project_two_root_folder_id,
|
||||
'some-harmless-doc.txt',
|
||||
(error, doc_id) =>
|
||||
cb(error)
|
||||
], done
|
||||
|
||||
it 'should produce a list of the users projects', (done) ->
|
||||
@owner.request.get {
|
||||
url: "/user/projects",
|
||||
json: true
|
||||
}, (err, response, body) =>
|
||||
expect(err).to.not.exist
|
||||
expect(body).to.deep.equal {
|
||||
projects: [
|
||||
{ _id: @project_one_id, name: 'plf-test-one', accessLevel: 'owner' },
|
||||
{ _id: @project_two_id, name: 'plf-test-two', accessLevel: 'owner' }
|
||||
]
|
||||
}
|
||||
done()
|
||||
|
||||
it 'should produce a list of entities in the project', (done) ->
|
||||
@owner.request.get {
|
||||
url: "/project/#{@project_two_id}/entities",
|
||||
json: true
|
||||
}, (err, response, body) =>
|
||||
expect(err).to.not.exist
|
||||
expect(body).to.deep.equal {
|
||||
project_id: @project_two_id,
|
||||
entities: [
|
||||
{ path: '/main.tex', type: 'doc' },
|
||||
{ path: '/some-harmless-doc.txt', type: 'doc' },
|
||||
{ path: '/test.txt', type: 'doc' }
|
||||
]
|
||||
}
|
||||
done()
|
||||
|
||||
|
||||
it 'should import a file from the source project', (done) ->
|
||||
@owner.request.post {
|
||||
url: "/project/#{@project_one_id}/linked_file",
|
||||
json:
|
||||
name: 'test-link.txt',
|
||||
parent_folder_id: @project_one_root_folder_id,
|
||||
provider: 'project_file',
|
||||
data:
|
||||
source_project_id: @project_two_id,
|
||||
source_entity_path: "/#{@source_doc_name}",
|
||||
}, (error, response, body) =>
|
||||
new_file_id = body.new_file_id
|
||||
@existing_file_id = new_file_id
|
||||
expect(new_file_id).to.exist
|
||||
@owner.getProject @project_one_id, (error, project) =>
|
||||
return done(error) if error?
|
||||
firstFile = project.rootFolder[0].fileRefs[0]
|
||||
expect(firstFile._id.toString()).to.equal(new_file_id.toString())
|
||||
expect(firstFile.linkedFileData).to.deep.equal {
|
||||
provider: 'project_file',
|
||||
source_project_id: @project_two_id,
|
||||
source_entity_path: "/#{@source_doc_name}",
|
||||
source_project_display_name: "plf-test-two"
|
||||
}
|
||||
expect(firstFile.name).to.equal('test-link.txt')
|
||||
done()
|
||||
|
||||
it 'should refresh the file', (done) ->
|
||||
@owner.request.post {
|
||||
url: "/project/#{@project_one_id}/linked_file",
|
||||
json:
|
||||
name: 'test-link.txt',
|
||||
parent_folder_id: @project_one_root_folder_id,
|
||||
provider: 'project_file',
|
||||
data:
|
||||
source_project_id: @project_two_id,
|
||||
source_entity_path: "/#{@source_doc_name}",
|
||||
}, (error, response, body) =>
|
||||
new_file_id = body.new_file_id
|
||||
expect(new_file_id).to.exist
|
||||
expect(new_file_id).to.not.equal @existing_file_id
|
||||
@owner.getProject @project_one_id, (error, project) =>
|
||||
return done(error) if error?
|
||||
firstFile = project.rootFolder[0].fileRefs[0]
|
||||
expect(firstFile._id.toString()).to.equal(new_file_id.toString())
|
||||
expect(firstFile.name).to.equal('test-link.txt')
|
||||
done()
|
||||
|
||||
describe "creating a URL based linked file", ->
|
||||
before (done) ->
|
||||
@owner.createProject "url-linked-files-project", {template: "blank"}, (error, project_id) =>
|
||||
|
@ -50,7 +166,7 @@ describe "LinkedFiles", ->
|
|||
name: 'url-test-file-1'
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
expect(response.statusCode).to.equal 200
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
file = project.rootFolder[0].fileRefs[0]
|
||||
|
@ -76,7 +192,7 @@ describe "LinkedFiles", ->
|
|||
name: 'url-test-file-2'
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
expect(response.statusCode).to.equal 200
|
||||
@owner.request.post {
|
||||
url: "/project/#{@project_id}/linked_file",
|
||||
json:
|
||||
|
@ -88,7 +204,7 @@ describe "LinkedFiles", ->
|
|||
name: 'url-test-file-2'
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
expect(response.statusCode).to.equal 200
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
file = project.rootFolder[0].fileRefs[1]
|
||||
|
@ -168,7 +284,7 @@ describe "LinkedFiles", ->
|
|||
name: 'url-test-file-6'
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
expect(response.statusCode).to.equal 200
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
file = _.find project.rootFolder[0].fileRefs, (file) ->
|
||||
|
|
|
@ -143,6 +143,18 @@ class User
|
|||
return callback(err)
|
||||
callback(null)
|
||||
|
||||
createDocInProject: (project_id, parent_folder_id, name, callback=(error, doc_id)->) ->
|
||||
@getCsrfToken (error) =>
|
||||
return callback(error) if error?
|
||||
@request.post {
|
||||
url: "/project/#{project_id}/doc",
|
||||
json: {
|
||||
name: name,
|
||||
parent_folder_id: parent_folder_id
|
||||
}
|
||||
}, (error, response, body) =>
|
||||
callback(null, body._id)
|
||||
|
||||
addUserToProject: (project_id, user, privileges, callback = (error, user) ->) ->
|
||||
if privileges == 'readAndWrite'
|
||||
updateOp = {$addToSet: {collaberator_refs: user._id.toString()}}
|
||||
|
|
|
@ -23,8 +23,8 @@ describe "BetaProgramController", ->
|
|||
optIn: sinon.stub()
|
||||
optOut: sinon.stub()
|
||||
},
|
||||
"../User/UserLocator": @UserLocator = {
|
||||
findById: sinon.stub()
|
||||
"../User/UserGetter": @UserGetter = {
|
||||
getUser: sinon.stub()
|
||||
},
|
||||
"settings-sharelatex": @settings = {
|
||||
languages: {}
|
||||
|
@ -119,7 +119,7 @@ describe "BetaProgramController", ->
|
|||
describe "optInPage", ->
|
||||
|
||||
beforeEach ->
|
||||
@UserLocator.findById.callsArgWith(1, null, @user)
|
||||
@UserGetter.getUser.callsArgWith(1, null, @user)
|
||||
|
||||
it "should render the opt-in page", () ->
|
||||
@BetaProgramController.optInPage @req, @res, @next
|
||||
|
@ -128,10 +128,10 @@ describe "BetaProgramController", ->
|
|||
args[0].should.equal 'beta_program/opt_in'
|
||||
|
||||
|
||||
describe "when UserLocator.findById produces an error", ->
|
||||
describe "when UserGetter.getUser produces an error", ->
|
||||
|
||||
beforeEach ->
|
||||
@UserLocator.findById.callsArgWith(1, new Error('woops'))
|
||||
@UserGetter.getUser.callsArgWith(1, new Error('woops'))
|
||||
|
||||
it "should not render the opt-in page", () ->
|
||||
@BetaProgramController.optInPage @req, @res, @next
|
||||
|
|
|
@ -24,11 +24,14 @@ describe "CollaboratorsInviteController", ->
|
|||
addCount: sinon.stub
|
||||
|
||||
@LimitationsManager = {}
|
||||
@UserGetter =
|
||||
getUserByMainEmail: sinon.stub()
|
||||
getUser: sinon.stub()
|
||||
|
||||
@CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
|
||||
"../Project/ProjectGetter": @ProjectGetter = {}
|
||||
'../Subscription/LimitationsManager' : @LimitationsManager
|
||||
'../User/UserGetter': @UserGetter = {getUser: sinon.stub()}
|
||||
'../User/UserGetter': @UserGetter
|
||||
"./CollaboratorsHandler": @CollaboratorsHandler = {}
|
||||
"./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
|
||||
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
|
||||
|
@ -713,7 +716,7 @@ describe "CollaboratorsInviteController", ->
|
|||
|
||||
beforeEach ->
|
||||
@user = {_id: ObjectId().toString()}
|
||||
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
|
||||
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, null, @user)
|
||||
|
||||
it 'should callback with `true`', (done) ->
|
||||
@call (err, shouldAllow) =>
|
||||
|
@ -725,7 +728,7 @@ describe "CollaboratorsInviteController", ->
|
|||
|
||||
beforeEach ->
|
||||
@user = null
|
||||
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
|
||||
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, null, @user)
|
||||
|
||||
it 'should callback with `false`', (done) ->
|
||||
@call (err, shouldAllow) =>
|
||||
|
@ -735,15 +738,15 @@ describe "CollaboratorsInviteController", ->
|
|||
|
||||
it 'should have called getUser', (done) ->
|
||||
@call (err, shouldAllow) =>
|
||||
@UserGetter.getUser.callCount.should.equal 1
|
||||
@UserGetter.getUser.calledWith({email: @email}, {_id: 1}).should.equal true
|
||||
@UserGetter.getUserByMainEmail.callCount.should.equal 1
|
||||
@UserGetter.getUserByMainEmail.calledWith(@email, {_id: 1}).should.equal true
|
||||
done()
|
||||
|
||||
describe 'when getUser produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@user = null
|
||||
@UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
|
||||
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, new Error('woops'))
|
||||
|
||||
it 'should callback with an error', (done) ->
|
||||
@call (err, shouldAllow) =>
|
||||
|
|
|
@ -605,7 +605,7 @@ describe "CollaboratorsInviteHandler", ->
|
|||
_id: ObjectId()
|
||||
first_name: "jim"
|
||||
@existingUser = {_id: ObjectId()}
|
||||
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @existingUser)
|
||||
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, null, @existingUser)
|
||||
@fakeProject =
|
||||
_id: @project_id
|
||||
name: "some project"
|
||||
|
@ -626,8 +626,8 @@ describe "CollaboratorsInviteHandler", ->
|
|||
|
||||
it 'should call getUser', (done) ->
|
||||
@call (err) =>
|
||||
@UserGetter.getUser.callCount.should.equal 1
|
||||
@UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
|
||||
@UserGetter.getUserByMainEmail.callCount.should.equal 1
|
||||
@UserGetter.getUserByMainEmail.calledWith(@invite.email).should.equal true
|
||||
done()
|
||||
|
||||
it 'should call getProject', (done) ->
|
||||
|
@ -671,7 +671,7 @@ describe "CollaboratorsInviteHandler", ->
|
|||
describe 'when the user does not exist', ->
|
||||
|
||||
beforeEach ->
|
||||
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
|
||||
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, null, null)
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err) =>
|
||||
|
@ -680,8 +680,8 @@ describe "CollaboratorsInviteHandler", ->
|
|||
|
||||
it 'should call getUser', (done) ->
|
||||
@call (err) =>
|
||||
@UserGetter.getUser.callCount.should.equal 1
|
||||
@UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
|
||||
@UserGetter.getUserByMainEmail.callCount.should.equal 1
|
||||
@UserGetter.getUserByMainEmail.calledWith(@invite.email).should.equal true
|
||||
done()
|
||||
|
||||
it 'should not call getProject', (done) ->
|
||||
|
@ -698,7 +698,7 @@ describe "CollaboratorsInviteHandler", ->
|
|||
describe 'when the getUser produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
|
||||
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err) =>
|
||||
|
@ -707,8 +707,8 @@ describe "CollaboratorsInviteHandler", ->
|
|||
|
||||
it 'should call getUser', (done) ->
|
||||
@call (err) =>
|
||||
@UserGetter.getUser.callCount.should.equal 1
|
||||
@UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
|
||||
@UserGetter.getUserByMainEmail.callCount.should.equal 1
|
||||
@UserGetter.getUserByMainEmail.calledWith(@invite.email).should.equal true
|
||||
done()
|
||||
|
||||
it 'should not call getProject', (done) ->
|
||||
|
|
|
@ -16,7 +16,7 @@ describe "PasswordResetHandler", ->
|
|||
getNewToken:sinon.stub()
|
||||
getValueFromTokenAndExpire:sinon.stub()
|
||||
@UserGetter =
|
||||
getUser:sinon.stub()
|
||||
getUserByMainEmail:sinon.stub()
|
||||
@EmailHandler =
|
||||
sendEmail:sinon.stub()
|
||||
@AuthenticationManager =
|
||||
|
@ -40,7 +40,7 @@ describe "PasswordResetHandler", ->
|
|||
describe "generateAndEmailResetToken", ->
|
||||
|
||||
it "should check the user exists", (done)->
|
||||
@UserGetter.getUser.callsArgWith(1)
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1)
|
||||
@OneTimeTokenHandler.getNewToken.callsArgWith(1)
|
||||
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
|
||||
exists.should.equal false
|
||||
|
@ -49,7 +49,7 @@ describe "PasswordResetHandler", ->
|
|||
|
||||
it "should send the email with the token", (done)->
|
||||
|
||||
@UserGetter.getUser.callsArgWith(1, null, @user)
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
|
||||
@OneTimeTokenHandler.getNewToken.callsArgWith(1, null, @token)
|
||||
@EmailHandler.sendEmail.callsArgWith(2)
|
||||
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
|
||||
|
@ -62,7 +62,7 @@ describe "PasswordResetHandler", ->
|
|||
|
||||
it "should return exists = false for a holdingAccount", (done) ->
|
||||
@user.holdingAccount = true
|
||||
@UserGetter.getUser.callsArgWith(1, null, @user)
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
|
||||
@OneTimeTokenHandler.getNewToken.callsArgWith(1)
|
||||
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
|
||||
exists.should.equal false
|
||||
|
|
|
@ -67,6 +67,7 @@ describe "ProjectController", ->
|
|||
protectTokens: sinon.stub()
|
||||
@CollaboratorsHandler =
|
||||
userIsTokenMember: sinon.stub().callsArgWith(2, null, false)
|
||||
@ProjectEntityHandler = {}
|
||||
@Modules =
|
||||
hooks:
|
||||
fire: sinon.stub()
|
||||
|
@ -98,6 +99,7 @@ describe "ProjectController", ->
|
|||
"../TokenAccess/TokenAccessHandler": @TokenAccessHandler
|
||||
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
|
||||
"../../infrastructure/Modules": @Modules
|
||||
"./ProjectEntityHandler": @ProjectEntityHandler
|
||||
|
||||
@projectName = "£12321jkj9ujkljds"
|
||||
@req =
|
||||
|
@ -520,7 +522,62 @@ describe "ProjectController", ->
|
|||
@ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true
|
||||
done()
|
||||
@ProjectController.loadEditor @req, @res
|
||||
|
||||
|
||||
describe 'userProjectsJson', ->
|
||||
beforeEach (done) ->
|
||||
projects = [
|
||||
{archived: true, id: 'a', name: 'A', accessLevel: 'a', somethingElse: 1}
|
||||
{archived: false, id: 'b', name: 'B', accessLevel: 'b', somethingElse: 1}
|
||||
{archived: false, id: 'c', name: 'C', accessLevel: 'c', somethingElse: 1}
|
||||
{archived: false, id: 'd', name: 'D', accessLevel: 'd', somethingElse: 1}
|
||||
]
|
||||
@ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, [])
|
||||
@ProjectController._buildProjectList = sinon.stub().returns(projects)
|
||||
@AuthenticationController.getLoggedInUserId = sinon.stub().returns 'abc'
|
||||
done()
|
||||
|
||||
it 'should produce a list of projects', (done) ->
|
||||
@res.json = (data) =>
|
||||
expect(data).to.deep.equal {
|
||||
projects: [
|
||||
{_id: 'b', name: 'B', accessLevel: 'b'},
|
||||
{_id: 'c', name: 'C', accessLevel: 'c'},
|
||||
{_id: 'd', name: 'D', accessLevel: 'd'}
|
||||
]
|
||||
}
|
||||
done()
|
||||
@ProjectController.userProjectsJson @req, @res, @next
|
||||
|
||||
describe 'projectEntitiesJson', ->
|
||||
beforeEach () ->
|
||||
@AuthenticationController.getLoggedInUserId = sinon.stub().returns 'abc'
|
||||
@req.params = {Project_id: 'abcd'}
|
||||
@project = { _id: 'abcd' }
|
||||
@docs = [
|
||||
{path: '/things/b.txt', doc: true},
|
||||
{path: '/main.tex', doc: true}
|
||||
]
|
||||
@files = [
|
||||
{path: '/things/a.txt'}
|
||||
]
|
||||
@ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @project)
|
||||
@ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub().callsArgWith(1, null, @docs, @files)
|
||||
|
||||
it 'should produce a list of entities', (done) ->
|
||||
@res.json = (data) =>
|
||||
expect(data).to.deep.equal {
|
||||
project_id: 'abcd',
|
||||
entities: [
|
||||
{path: '/main.tex', type: 'doc'},
|
||||
{path: '/things/a.txt', type: 'file'},
|
||||
{path: '/things/b.txt', type: 'doc'}
|
||||
]
|
||||
}
|
||||
expect(@ProjectGetter.getProject.callCount).to.equal 1
|
||||
expect(@ProjectEntityHandler.getAllEntitiesFromProject.callCount).to.equal 1
|
||||
done()
|
||||
@ProjectController.projectEntitiesJson @req, @res, @next
|
||||
|
||||
describe '_isInPercentageRollout', ->
|
||||
before ->
|
||||
@ids = [
|
||||
|
|
|
@ -24,9 +24,6 @@ describe "SubscriptionGroupHandler", ->
|
|||
getSubscriptionByMemberIdAndId: sinon.stub()
|
||||
getSubscription: sinon.stub()
|
||||
|
||||
@UserCreator =
|
||||
getUserOrCreateHoldingAccount: sinon.stub().callsArgWith(1, null, @user)
|
||||
|
||||
@SubscriptionUpdater =
|
||||
addUserToGroup: sinon.stub().callsArgWith(2)
|
||||
removeUserFromGroup: sinon.stub().callsArgWith(2)
|
||||
|
@ -34,9 +31,9 @@ describe "SubscriptionGroupHandler", ->
|
|||
@TeamInvitesHandler =
|
||||
createManagerInvite: sinon.stub().callsArgWith(2)
|
||||
|
||||
@UserLocator =
|
||||
findById: sinon.stub()
|
||||
findByEmail: sinon.stub()
|
||||
@UserGetter =
|
||||
getUser: sinon.stub()
|
||||
getUserByMainEmail: sinon.stub()
|
||||
|
||||
@LimitationsManager =
|
||||
hasGroupMembersLimitReached: sinon.stub()
|
||||
|
@ -61,7 +58,7 @@ describe "SubscriptionGroupHandler", ->
|
|||
"./SubscriptionUpdater": @SubscriptionUpdater
|
||||
"./TeamInvitesHandler": @TeamInvitesHandler
|
||||
"./SubscriptionLocator": @SubscriptionLocator
|
||||
"../User/UserLocator": @UserLocator
|
||||
"../User/UserGetter": @UserGetter
|
||||
"./LimitationsManager": @LimitationsManager
|
||||
"../Security/OneTimeTokenHandler":@OneTimeTokenHandler
|
||||
"../Email/EmailHandler":@EmailHandler
|
||||
|
@ -76,11 +73,11 @@ describe "SubscriptionGroupHandler", ->
|
|||
describe "addUserToGroup", ->
|
||||
beforeEach ->
|
||||
@LimitationsManager.hasGroupMembersLimitReached.callsArgWith(1, null, false, @subscription)
|
||||
@UserLocator.findByEmail.callsArgWith(1, null, @user)
|
||||
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
|
||||
|
||||
it "should find the user", (done)->
|
||||
@Handler.addUserToGroup @adminUser_id, @newEmail, (err)=>
|
||||
@UserLocator.findByEmail.calledWith(@newEmail).should.equal true
|
||||
@UserGetter.getUserByMainEmail.calledWith(@newEmail).should.equal true
|
||||
done()
|
||||
|
||||
it "should add the user to the group", (done)->
|
||||
|
@ -105,9 +102,9 @@ describe "SubscriptionGroupHandler", ->
|
|||
@NotificationsBuilder.groupPlan.calledWith(@user, {subscription_id:@subscription._id}).should.equal true
|
||||
@readStub.called.should.equal true
|
||||
done()
|
||||
|
||||
it "should add a team invite if no user is found", (done) ->
|
||||
@UserLocator.findByEmail.callsArgWith(1, null, null)
|
||||
|
||||
it "should add an email invite if no user is found", (done) ->
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1, null, null)
|
||||
@Handler.addUserToGroup @adminUser_id, @newEmail, (err)=>
|
||||
@TeamInvitesHandler.createManagerInvite.calledWith(@adminUser_id, @newEmail).should.equal true
|
||||
done()
|
||||
|
@ -124,26 +121,26 @@ describe "SubscriptionGroupHandler", ->
|
|||
beforeEach ->
|
||||
@subscription = {}
|
||||
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription)
|
||||
@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
|
||||
@UserGetter.getUser.callsArgWith(1, null, {_id:"31232"})
|
||||
|
||||
it "should locate the subscription", (done)->
|
||||
@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
|
||||
@UserGetter.getUser.callsArgWith(1, null, {_id:"31232"})
|
||||
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
|
||||
@SubscriptionLocator.getUsersSubscription.calledWith(@adminUser_id).should.equal true
|
||||
done()
|
||||
|
||||
it "should get the users by id", (done)->
|
||||
@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
|
||||
@UserGetter.getUser.callsArgWith(1, null, {_id:"31232"})
|
||||
@subscription.member_ids = ["1234", "342432", "312312"]
|
||||
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
|
||||
@UserLocator.findById.calledWith(@subscription.member_ids[0]).should.equal true
|
||||
@UserLocator.findById.calledWith(@subscription.member_ids[1]).should.equal true
|
||||
@UserLocator.findById.calledWith(@subscription.member_ids[2]).should.equal true
|
||||
@UserGetter.getUser.calledWith(@subscription.member_ids[0]).should.equal true
|
||||
@UserGetter.getUser.calledWith(@subscription.member_ids[1]).should.equal true
|
||||
@UserGetter.getUser.calledWith(@subscription.member_ids[2]).should.equal true
|
||||
users.length.should.equal @subscription.member_ids.length
|
||||
done()
|
||||
|
||||
it "should just return the id if the user can not be found as they may have deleted their account", (done)->
|
||||
@UserLocator.findById.callsArgWith(1)
|
||||
@UserGetter.getUser.callsArgWith(1)
|
||||
@subscription.member_ids = ["1234", "342432", "312312"]
|
||||
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
|
||||
assert.deepEqual users[0], {_id:@subscription.member_ids[0]}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
should = require('chai').should()
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
assert = require('assert')
|
||||
path = require('path')
|
||||
sinon = require('sinon')
|
||||
modulePath = '../../../../app/js/Features/Templates/TemplatesController'
|
||||
|
||||
|
||||
describe 'TemplatesController', ->
|
||||
|
||||
project_id = "213432"
|
||||
|
||||
beforeEach ->
|
||||
@request = sinon.stub()
|
||||
@request.returns {
|
||||
pipe:->
|
||||
on:->
|
||||
}
|
||||
@fs = {
|
||||
unlink : sinon.stub()
|
||||
createWriteStream : sinon.stub().returns(on:(_, cb)->cb())
|
||||
}
|
||||
@ProjectUploadManager = {createProjectFromZipArchive : sinon.stub().callsArgWith(3, null, {_id:project_id})}
|
||||
@dumpFolder = "dump/path"
|
||||
@ProjectOptionsHandler = {setCompiler:sinon.stub().callsArgWith(2)}
|
||||
@uuid = "1234"
|
||||
@ProjectDetailsHandler =
|
||||
getProjectDescription:sinon.stub()
|
||||
@Project =
|
||||
update: sinon.stub().callsArgWith(3, null)
|
||||
@controller = SandboxedModule.require modulePath, requires:
|
||||
'../../../js/Features/Uploads/ProjectUploadManager':@ProjectUploadManager
|
||||
'../../../js/Features/Project/ProjectOptionsHandler':@ProjectOptionsHandler
|
||||
'../../../js/Features/Authentication/AuthenticationController': @AuthenticationController = {getLoggedInUserId: sinon.stub()}
|
||||
'./TemplatesPublisher':@TemplatesPublisher
|
||||
"logger-sharelatex":
|
||||
log:->
|
||||
err:->
|
||||
"settings-sharelatex":
|
||||
path:
|
||||
dumpFolder:@dumpFolder
|
||||
siteUrl: @siteUrl = "http://localhost:3000"
|
||||
apis:
|
||||
v1:
|
||||
url: @v1Url="http://overleaf.com"
|
||||
user: "sharelatex"
|
||||
pass: "password"
|
||||
overleaf:
|
||||
host: @v1Url
|
||||
"uuid":v4:=>@uuid
|
||||
"request": @request
|
||||
"fs":@fs
|
||||
"../../../../app/js/models/Project": {Project: @Project}
|
||||
@zipUrl = "%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex"
|
||||
@templateName = "project name here"
|
||||
@user_id = "1234"
|
||||
@req =
|
||||
session:
|
||||
user: _id:@user_id
|
||||
templateData:
|
||||
zipUrl: @zipUrl
|
||||
templateName: @templateName
|
||||
@redirect = {}
|
||||
@AuthenticationController.getLoggedInUserId.returns(@user_id)
|
||||
|
||||
describe 'v1Templates', ->
|
||||
|
||||
it "should fetch zip from v1 based on template id", (done)->
|
||||
@templateVersionId = 15
|
||||
@req.body = {templateVersionId: @templateVersionId}
|
||||
|
||||
redirect = =>
|
||||
@request.calledWith("#{@v1Url}/api/v1/sharelatex/templates/#{@templateVersionId}").should.equal true
|
||||
done()
|
||||
res = redirect:redirect
|
||||
@controller.createProjectFromV1Template @req, res
|
|
@ -30,8 +30,8 @@ describe "UserController", ->
|
|||
|
||||
@UserDeleter =
|
||||
deleteUser: sinon.stub().callsArgWith(1)
|
||||
@UserLocator =
|
||||
findById: sinon.stub().callsArgWith(1, null, @user)
|
||||
@UserGetter =
|
||||
getUser: sinon.stub().callsArgWith(1, null, @user)
|
||||
@User =
|
||||
findById: sinon.stub().callsArgWith(1, null, @user)
|
||||
@NewsLetterManager =
|
||||
|
@ -63,7 +63,7 @@ describe "UserController", ->
|
|||
@SudoModeHandler =
|
||||
clearSudoMode: sinon.stub()
|
||||
@UserController = SandboxedModule.require modulePath, requires:
|
||||
"./UserLocator": @UserLocator
|
||||
"./UserGetter": @UserGetter
|
||||
"./UserDeleter": @UserDeleter
|
||||
"./UserUpdater":@UserUpdater
|
||||
"../../models/User": User:@User
|
||||
|
|
|
@ -15,34 +15,16 @@ describe "UserCreator", ->
|
|||
constructor: ->
|
||||
return self.user
|
||||
|
||||
@UserLocator =
|
||||
findByEmail: sinon.stub()
|
||||
@UserGetter =
|
||||
getUserByMainEmail: sinon.stub()
|
||||
@UserCreator = SandboxedModule.require modulePath, requires:
|
||||
"../../models/User": User:@UserModel
|
||||
"./UserLocator":@UserLocator
|
||||
"./UserGetter":@UserGetter
|
||||
"logger-sharelatex":{log:->}
|
||||
'metrics-sharelatex': {timeAsyncMethod: ()->}
|
||||
|
||||
@email = "bob.oswald@gmail.com"
|
||||
|
||||
|
||||
describe "getUserOrCreateHoldingAccount", ->
|
||||
|
||||
it "should immediately return the user if found", (done)->
|
||||
@UserLocator.findByEmail.callsArgWith(1, null, @user)
|
||||
@UserCreator.getUserOrCreateHoldingAccount @email, (err, returnedUser)=>
|
||||
assert.deepEqual returnedUser, @user
|
||||
done()
|
||||
|
||||
it "should create new holding account if the user is not found", (done)->
|
||||
@UserLocator.findByEmail.callsArgWith(1)
|
||||
@UserCreator.createNewUser = sinon.stub().callsArgWith(1, null, @user)
|
||||
@UserCreator.getUserOrCreateHoldingAccount @email, (err, returnedUser)=>
|
||||
@UserCreator.createNewUser.calledWith(email:@email, holdingAccount:true).should.equal true
|
||||
assert.deepEqual returnedUser, @user
|
||||
done()
|
||||
|
||||
|
||||
describe "createNewUser", ->
|
||||
|
||||
it "should take the opts and put them in the model", (done)->
|
||||
|
|
58
services/web/test/unit/coffee/User/UserGetterTests.coffee
Normal file
58
services/web/test/unit/coffee/User/UserGetterTests.coffee
Normal file
|
@ -0,0 +1,58 @@
|
|||
should = require('chai').should()
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
assert = require('assert')
|
||||
path = require('path')
|
||||
sinon = require('sinon')
|
||||
modulePath = path.join __dirname, "../../../../app/js/Features/User/UserGetter"
|
||||
expect = require("chai").expect
|
||||
|
||||
describe "UserGetter", ->
|
||||
|
||||
beforeEach ->
|
||||
@fakeUser = {_id:"12390i"}
|
||||
@findOne = sinon.stub().callsArgWith(2, null, @fakeUser)
|
||||
@Mongo =
|
||||
db: users: findOne: @findOne
|
||||
ObjectId: (id) -> return id
|
||||
|
||||
@UserGetter = SandboxedModule.require modulePath, requires:
|
||||
"logger-sharelatex": log:->
|
||||
"../../infrastructure/mongojs": @Mongo
|
||||
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
|
||||
|
||||
describe "getUser", ->
|
||||
it "should get user", (done)->
|
||||
query = _id: 'foo'
|
||||
projection = email: 1
|
||||
@UserGetter.getUser query, projection, (error, user) =>
|
||||
@findOne.called.should.equal true
|
||||
@findOne.calledWith(query, projection).should.equal true
|
||||
user.should.deep.equal @fakeUser
|
||||
done()
|
||||
|
||||
it "should not allow email in query", (done)->
|
||||
@UserGetter.getUser email: 'foo@bar.com', {}, (error, user) =>
|
||||
error.should.exist
|
||||
done()
|
||||
|
||||
describe "getUserbyMainEmail", ->
|
||||
it "query user by main email", (done)->
|
||||
email = 'hello@world.com'
|
||||
projection = emails: 1
|
||||
@UserGetter.getUserByMainEmail email, projection, (error, user) =>
|
||||
@findOne.called.should.equal true
|
||||
@findOne.calledWith(email: email, projection).should.equal true
|
||||
done()
|
||||
|
||||
it "return user if found", (done)->
|
||||
email = 'hello@world.com'
|
||||
@UserGetter.getUserByMainEmail email, (error, user) =>
|
||||
user.should.deep.equal @fakeUser
|
||||
done()
|
||||
|
||||
it "trim email", (done)->
|
||||
email = 'hello@world.com'
|
||||
@UserGetter.getUserByMainEmail " #{email} ", (error, user) =>
|
||||
@findOne.called.should.equal true
|
||||
@findOne.calledWith(email: email).should.equal true
|
||||
done()
|
|
@ -1,39 +0,0 @@
|
|||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
modulePath = "../../../../app/js/Features/User/UserLocator.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "UserLocator", ->
|
||||
|
||||
beforeEach ->
|
||||
@user = {_id:"12390i"}
|
||||
@UserLocator = SandboxedModule.require modulePath, requires:
|
||||
"../../infrastructure/mongojs": db: @db = { users: {} }
|
||||
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
|
||||
'logger-sharelatex' : { log: sinon.stub() }
|
||||
@db.users =
|
||||
findOne : sinon.stub().callsArgWith(1, null, @user)
|
||||
|
||||
@email = "bob.oswald@gmail.com"
|
||||
|
||||
|
||||
describe "findByEmail", ->
|
||||
|
||||
it "should try and find a user with that email address", (done)->
|
||||
@UserLocator.findByEmail @email, (err, user)=>
|
||||
@db.users.findOne.calledWith(email:@email).should.equal true
|
||||
done()
|
||||
|
||||
it "should trim white space", (done)->
|
||||
@UserLocator.findByEmail "#{@email} ", (err, user)=>
|
||||
@db.users.findOne.calledWith(email:@email).should.equal true
|
||||
done()
|
||||
|
||||
it "should return the user if found", (done)->
|
||||
@UserLocator.findByEmail @email, (err, user)=>
|
||||
user.should.deep.equal @user
|
||||
done()
|
||||
|
||||
|
||||
|
|
@ -16,10 +16,7 @@ describe "UserPagesController", ->
|
|||
features:{}
|
||||
email: "joe@example.com"
|
||||
|
||||
@UserLocator =
|
||||
findById: sinon.stub().callsArgWith(1, null, @user)
|
||||
@UserGetter =
|
||||
getUser: sinon.stub().callsArgWith(2, null, @user)
|
||||
@UserGetter = getUser: sinon.stub()
|
||||
@UserSessionsManager =
|
||||
getAllUserSessions: sinon.stub()
|
||||
@dropboxStatus = {}
|
||||
|
@ -37,7 +34,6 @@ describe "UserPagesController", ->
|
|||
"logger-sharelatex":
|
||||
log:->
|
||||
err:->
|
||||
"./UserLocator": @UserLocator
|
||||
"./UserGetter": @UserGetter
|
||||
"./UserSessionsManager": @UserSessionsManager
|
||||
"../Errors/ErrorController": @ErrorController
|
||||
|
@ -136,6 +132,8 @@ describe "UserPagesController", ->
|
|||
@UserPagesController.sessionsPage @req, @res, @next
|
||||
|
||||
describe "settingsPage", ->
|
||||
beforeEach ->
|
||||
@UserGetter.getUser = sinon.stub().callsArgWith(1, null, @user)
|
||||
|
||||
it "should render user/settings", (done)->
|
||||
@res.render = (page)->
|
||||
|
@ -185,6 +183,7 @@ describe "UserPagesController", ->
|
|||
|
||||
describe "activateAccountPage", ->
|
||||
beforeEach ->
|
||||
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
|
||||
@req.query.user_id = @user_id
|
||||
@req.query.token = @token = "mock-token-123"
|
||||
|
||||
|
|
|
@ -12,8 +12,9 @@ describe "UserRegistrationHandler", ->
|
|||
@user =
|
||||
_id: @user_id = "31j2lk21kjl"
|
||||
@User =
|
||||
findOne:sinon.stub()
|
||||
update: sinon.stub().callsArgWith(2)
|
||||
@UserGetter =
|
||||
getUserByMainEmail: sinon.stub()
|
||||
@UserCreator =
|
||||
createNewUser:sinon.stub().callsArgWith(1, null, @user)
|
||||
@AuthenticationManager =
|
||||
|
@ -26,6 +27,7 @@ describe "UserRegistrationHandler", ->
|
|||
getNewToken: sinon.stub()
|
||||
@handler = SandboxedModule.require modulePath, requires:
|
||||
"../../models/User": {User:@User}
|
||||
"./UserGetter": @UserGetter
|
||||
"./UserCreator": @UserCreator
|
||||
"../Authentication/AuthenticationManager":@AuthenticationManager
|
||||
"../Newsletter/NewsletterManager":@NewsLetterManager
|
||||
|
@ -70,7 +72,7 @@ describe "UserRegistrationHandler", ->
|
|||
beforeEach ->
|
||||
@user.holdingAccount = true
|
||||
@handler._registrationRequestIsValid = sinon.stub().returns true
|
||||
@User.findOne.callsArgWith(1, null, @user)
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
|
||||
|
||||
it "should not create a new user if there is a holding account there", (done)->
|
||||
@handler.registerNewUser @passingRequest, (err)=>
|
||||
|
@ -94,7 +96,7 @@ describe "UserRegistrationHandler", ->
|
|||
done()
|
||||
|
||||
it "should return email registered in the error if there is a non holdingAccount there", (done)->
|
||||
@User.findOne.callsArgWith(1, null, @user = {holdingAccount:false})
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user = {holdingAccount:false})
|
||||
@handler.registerNewUser @passingRequest, (err, user)=>
|
||||
err.should.deep.equal new Error("EmailAlreadyRegistered")
|
||||
user.should.deep.equal @user
|
||||
|
@ -103,7 +105,7 @@ describe "UserRegistrationHandler", ->
|
|||
describe "validRequest", ->
|
||||
beforeEach ->
|
||||
@handler._registrationRequestIsValid = sinon.stub().returns true
|
||||
@User.findOne.callsArgWith 1
|
||||
@UserGetter.getUserByMainEmail.callsArgWith 1
|
||||
|
||||
it "should create a new user", (done)->
|
||||
@handler.registerNewUser @passingRequest, (err)=>
|
||||
|
|
|
@ -14,12 +14,12 @@ describe "UserUpdater", ->
|
|||
@mongojs =
|
||||
db:{}
|
||||
ObjectId:(id)-> return id
|
||||
@UserLocator =
|
||||
findByEmail:sinon.stub()
|
||||
@UserGetter =
|
||||
getUserByMainEmail: sinon.stub()
|
||||
@UserUpdater = SandboxedModule.require modulePath, requires:
|
||||
"settings-sharelatex":@settings
|
||||
"logger-sharelatex": log:->
|
||||
"./UserLocator":@UserLocator
|
||||
"./UserGetter": @UserGetter
|
||||
"../../infrastructure/mongojs":@mongojs
|
||||
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
|
||||
|
||||
|
@ -34,7 +34,7 @@ describe "UserUpdater", ->
|
|||
@UserUpdater.updateUser = sinon.stub().callsArgWith(2)
|
||||
|
||||
it "should check if the new email already has an account", (done)->
|
||||
@UserLocator.findByEmail.callsArgWith(1, null, @stubbedUser)
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @stubbedUser)
|
||||
@UserUpdater.changeEmailAddress @user_id, @stubbedUser.email, (err)=>
|
||||
@UserUpdater.updateUser.called.should.equal false
|
||||
should.exist(err)
|
||||
|
@ -42,7 +42,7 @@ describe "UserUpdater", ->
|
|||
|
||||
|
||||
it "should set the users password", (done)->
|
||||
@UserLocator.findByEmail.callsArgWith(1, null)
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1, null)
|
||||
@UserUpdater.changeEmailAddress @user_id, @newEmail, (err)=>
|
||||
@UserUpdater.updateUser.calledWith(@user_id, $set: { "email": @newEmail}).should.equal true
|
||||
done()
|
||||
|
|
Loading…
Add table
Reference in a new issue