mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #579 from sharelatex/sk-linked-files-from-project
Linked Files from Project
This commit is contained in:
commit
d3ae276091
20 changed files with 703 additions and 43 deletions
|
@ -5,7 +5,8 @@ logger = require 'logger-sharelatex'
|
||||||
|
|
||||||
module.exports = LinkedFilesController = {
|
module.exports = LinkedFilesController = {
|
||||||
Agents: {
|
Agents: {
|
||||||
url: require('./UrlAgent')
|
url: require('./UrlAgent'),
|
||||||
|
project_file: require('./ProjectFileAgent')
|
||||||
}
|
}
|
||||||
|
|
||||||
createLinkedFile: (req, res, next) ->
|
createLinkedFile: (req, res, next) ->
|
||||||
|
@ -22,11 +23,17 @@ module.exports = LinkedFilesController = {
|
||||||
|
|
||||||
linkedFileData = Agent.sanitizeData(data)
|
linkedFileData = Agent.sanitizeData(data)
|
||||||
linkedFileData.provider = provider
|
linkedFileData.provider = provider
|
||||||
Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) ->
|
Agent.checkAuth project_id, linkedFileData, user_id, (err, allowed) ->
|
||||||
if error?
|
return Agent.handleError(err, req, res, next) if err?
|
||||||
logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk'
|
return res.sendStatus(403) if !allowed
|
||||||
return Agent.handleError(error, req, res, next)
|
Agent.decorateLinkedFileData linkedFileData, (err, newLinkedFileData) ->
|
||||||
EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error) ->
|
return Agent.handleError(err) if err?
|
||||||
return next(error) if error?
|
linkedFileData = newLinkedFileData
|
||||||
res.send(204) # created
|
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)
|
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) ->) ->
|
writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) ->
|
||||||
callback = _.once(callback)
|
callback = _.once(callback)
|
||||||
url = data.url
|
url = data.url
|
||||||
|
@ -65,4 +71,4 @@ module.exports = UrlAgent = {
|
||||||
if !Settings.apis?.linkedUrlProxy?.url?
|
if !Settings.apis?.linkedUrlProxy?.url?
|
||||||
throw new Error('no linked url proxy configured')
|
throw new Error('no linked url proxy configured')
|
||||||
return "#{Settings.apis.linkedUrlProxy.url}?url=#{encodeURIComponent(url)}"
|
return "#{Settings.apis.linkedUrlProxy.url}?url=#{encodeURIComponent(url)}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ Sources = require "../Authorization/Sources"
|
||||||
TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
|
TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
|
||||||
CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
|
CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
|
||||||
Modules = require '../../infrastructure/Modules'
|
Modules = require '../../infrastructure/Modules'
|
||||||
|
ProjectEntityHandler = require './ProjectEntityHandler'
|
||||||
crypto = require 'crypto'
|
crypto = require 'crypto'
|
||||||
|
|
||||||
module.exports = ProjectController =
|
module.exports = ProjectController =
|
||||||
|
@ -138,6 +139,33 @@ module.exports = ProjectController =
|
||||||
return next(err) if err?
|
return next(err) if err?
|
||||||
res.sendStatus 200
|
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)->
|
projectListPage: (req, res, next)->
|
||||||
timer = new metrics.Timer("project-list")
|
timer = new metrics.Timer("project-list")
|
||||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||||
|
@ -313,6 +341,7 @@ module.exports = ProjectController =
|
||||||
maxDocLength: Settings.max_doc_length
|
maxDocLength: Settings.max_doc_length
|
||||||
useV2History: !!project.overleaf?.history?.display
|
useV2History: !!project.overleaf?.history?.display
|
||||||
showRichText: req.query?.rt == 'true'
|
showRichText: req.query?.rt == 'true'
|
||||||
|
showTestControls: req.query?.tc == 'true' || user.isAdmin
|
||||||
showPublishModal: req.query?.pm == 'true'
|
showPublishModal: req.query?.pm == 'true'
|
||||||
timer.done()
|
timer.done()
|
||||||
|
|
||||||
|
|
|
@ -6,16 +6,31 @@ Settings = require 'settings-sharelatex'
|
||||||
request = require 'request'
|
request = require 'request'
|
||||||
|
|
||||||
module.exports = FileWriter =
|
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) ->) ->
|
writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) ->
|
||||||
callback = _.once(callback)
|
callback = _.once(callback)
|
||||||
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
|
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
|
||||||
|
|
||||||
stream.pause()
|
stream.pause()
|
||||||
fs.mkdir Settings.path.dumpFolder, (error) ->
|
FileWriter._ensureDumpFolderExists (error) ->
|
||||||
|
return callback(error) if error?
|
||||||
stream.resume()
|
stream.resume()
|
||||||
if error? and error.code != 'EEXIST'
|
|
||||||
# Ignore error about already existing
|
|
||||||
return callback(error)
|
|
||||||
|
|
||||||
writeStream = fs.createWriteStream(fsPath)
|
writeStream = fs.createWriteStream(fsPath)
|
||||||
stream.pipe(writeStream)
|
stream.pipe(writeStream)
|
||||||
|
@ -39,4 +54,4 @@ module.exports = FileWriter =
|
||||||
else
|
else
|
||||||
err = new Error("bad response from url: #{response.statusCode}")
|
err = new Error("bad response from url: #{response.statusCode}")
|
||||||
logger.err {err, identifier, url}, err.message
|
logger.err {err, identifier, url}, err.message
|
||||||
callback(err)
|
callback(err)
|
||||||
|
|
|
@ -119,6 +119,11 @@ module.exports = class Router
|
||||||
webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
|
webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
|
||||||
privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
|
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.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage
|
||||||
webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
|
webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,18 @@ div.binary-file.full-size(
|
||||||
|
|
|
|
||||||
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
|
| 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(
|
button.btn.btn-success(
|
||||||
href, ng-click="refreshFile(openFile)",
|
href, ng-click="refreshFile(openFile)",
|
||||||
ng-disabled="refreshing"
|
ng-disabled="refreshing"
|
||||||
|
@ -63,3 +74,7 @@ div.binary-file.full-size(
|
||||||
i.fa.fa-fw.fa-download
|
i.fa.fa-fw.fa-download
|
||||||
|
|
|
|
||||||
| #{translate("download")}
|
| #{translate("download")}
|
||||||
|
div(ng-if="refreshError")
|
||||||
|
br
|
||||||
|
.alert.alert-danger.col-md-6.col-md-offset-3
|
||||||
|
| Error: {{ refreshError}}
|
||||||
|
|
|
@ -342,6 +342,77 @@ script(type='text/ng-template', id='newDocModalTemplate')
|
||||||
span(ng-show="state.inflight") #{translate("creating")}...
|
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')
|
script(type='text/ng-template', id='linkedFileModalTemplate')
|
||||||
.modal-header
|
.modal-header
|
||||||
h3 New file from URL
|
h3 New file from URL
|
||||||
|
|
|
@ -62,6 +62,23 @@ aside#left-menu.full-size(
|
||||||
!= moduleIncludes("editorLeftMenu:editing_services", locals)
|
!= 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")}
|
h4(ng-show="!anonymous") #{translate("settings")}
|
||||||
form.settings(ng-controller="SettingsController", ng-show="!anonymous")
|
form.settings(ng-controller="SettingsController", ng-show="!anonymous")
|
||||||
.containter-fluid
|
.containter-fluid
|
||||||
|
@ -179,6 +196,7 @@ aside#left-menu.full-size(
|
||||||
option(value="pdfjs") #{translate("built_in")}
|
option(value="pdfjs") #{translate("built_in")}
|
||||||
option(value="native") #{translate("native")}
|
option(value="native") #{translate("native")}
|
||||||
|
|
||||||
|
|
||||||
h4 #{translate("hotkeys")}
|
h4 #{translate("hotkeys")}
|
||||||
ul.list-unstyled.nav
|
ul.list-unstyled.nav
|
||||||
li(ng-controller="HotkeysController")
|
li(ng-controller="HotkeysController")
|
||||||
|
|
|
@ -17,6 +17,7 @@ services:
|
||||||
PROJECT_HISTORY_ENABLED: 'true'
|
PROJECT_HISTORY_ENABLED: 'true'
|
||||||
ENABLED_LINKED_FILE_TYPES: 'url'
|
ENABLED_LINKED_FILE_TYPES: 'url'
|
||||||
LINKED_URL_PROXY: 'http://localhost:6543'
|
LINKED_URL_PROXY: 'http://localhost:6543'
|
||||||
|
ENABLED_LINKED_FILE_TYPES: 'url,project_file'
|
||||||
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
|
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
|
|
|
@ -18,6 +18,7 @@ define [
|
||||||
"ide/chat/index"
|
"ide/chat/index"
|
||||||
"ide/clone/index"
|
"ide/clone/index"
|
||||||
"ide/hotkeys/index"
|
"ide/hotkeys/index"
|
||||||
|
"ide/test-controls/index"
|
||||||
"ide/wordcount/index"
|
"ide/wordcount/index"
|
||||||
"ide/directives/layout"
|
"ide/directives/layout"
|
||||||
"ide/directives/validFile"
|
"ide/directives/validFile"
|
||||||
|
@ -34,6 +35,7 @@ define [
|
||||||
"directives/videoPlayState"
|
"directives/videoPlayState"
|
||||||
"services/queued-http"
|
"services/queued-http"
|
||||||
"services/validateCaptcha"
|
"services/validateCaptcha"
|
||||||
|
"services/wait-for"
|
||||||
"filters/formatDate"
|
"filters/formatDate"
|
||||||
"main/event"
|
"main/event"
|
||||||
"main/account-upgrade"
|
"main/account-upgrade"
|
||||||
|
@ -54,7 +56,7 @@ define [
|
||||||
SafariScrollPatcher
|
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
|
# Don't freak out if we're already in an apply callback
|
||||||
$scope.$originalApply = $scope.$apply
|
$scope.$originalApply = $scope.$apply
|
||||||
$scope.$apply = (fn = () ->) ->
|
$scope.$apply = (fn = () ->) ->
|
||||||
|
|
|
@ -2,7 +2,7 @@ define [
|
||||||
"base"
|
"base"
|
||||||
"moment"
|
"moment"
|
||||||
], (App, 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
|
TWO_MEGABYTES = 2 * 1024 * 1024
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ define [
|
||||||
data: null
|
data: null
|
||||||
|
|
||||||
$scope.refreshing = false
|
$scope.refreshing = false
|
||||||
|
$scope.refreshError = null
|
||||||
|
|
||||||
MAX_URL_LENGTH = 60
|
MAX_URL_LENGTH = 60
|
||||||
FRONT_OF_URL_LENGTH = 35
|
FRONT_OF_URL_LENGTH = 35
|
||||||
|
@ -48,9 +49,27 @@ define [
|
||||||
|
|
||||||
$scope.refreshFile = (file) ->
|
$scope.refreshFile = (file) ->
|
||||||
$scope.refreshing = true
|
$scope.refreshing = true
|
||||||
|
$scope.refreshError = null
|
||||||
ide.fileTreeManager.refreshLinkedFile(file)
|
ide.fileTreeManager.refreshLinkedFile(file)
|
||||||
.then () ->
|
.then (response) ->
|
||||||
loadTextFileFilePreview()
|
{ 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 () ->
|
.finally () ->
|
||||||
$scope.refreshing = false
|
$scope.refreshing = false
|
||||||
|
|
||||||
|
@ -86,11 +105,9 @@ define [
|
||||||
# show dots when payload is closs to cutoff
|
# show dots when payload is closs to cutoff
|
||||||
if data.length >= (TWO_MEGABYTES - 200)
|
if data.length >= (TWO_MEGABYTES - 200)
|
||||||
$scope.textPreview.shouldShowDots = true
|
$scope.textPreview.shouldShowDots = true
|
||||||
try
|
|
||||||
# remove last partial line
|
# remove last partial line
|
||||||
data = data.replace(/\n.*$/, '')
|
data = data?.replace?(/\n.*$/, '')
|
||||||
finally
|
$scope.textPreview.data = data
|
||||||
$scope.textPreview.data = data
|
|
||||||
$timeout(setHeight, 0)
|
$timeout(setHeight, 0)
|
||||||
.catch (error) ->
|
.catch (error) ->
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
|
@ -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) ->
|
$scope.orderByFoldersFirst = (entity) ->
|
||||||
return '0' if entity?.type == "folder"
|
return '0' if entity?.type == "folder"
|
||||||
return '1'
|
return '1'
|
||||||
|
@ -201,6 +214,117 @@ define [
|
||||||
$modalInstance.dismiss('cancel')
|
$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", [
|
App.controller "LinkedFileModalController", [
|
||||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
define [
|
define [
|
||||||
"base"
|
"base"
|
||||||
], (App) ->
|
], (App) ->
|
||||||
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking) ->
|
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking, waitFor) ->
|
||||||
$scope.restoreState =
|
$scope.restoreState =
|
||||||
inflight: false
|
inflight: false
|
||||||
error: false
|
error: false
|
||||||
|
@ -24,17 +24,16 @@ define [
|
||||||
$scope.restoreState.inflight = false
|
$scope.restoreState.inflight = false
|
||||||
|
|
||||||
openEntity = (data) ->
|
openEntity = (data) ->
|
||||||
iterations = 0
|
|
||||||
{id, type} = data
|
{id, type} = data
|
||||||
do tryOpen = () ->
|
waitFor(
|
||||||
if iterations > 5
|
() ->
|
||||||
return
|
ide.fileTreeManager.findEntityById(id)
|
||||||
iterations += 1
|
3000
|
||||||
entity = ide.fileTreeManager.findEntityById(id)
|
)
|
||||||
if entity? and type == 'doc'
|
.then (entity) ->
|
||||||
ide.editorManager.openDoc(entity)
|
if type == 'doc'
|
||||||
else if entity? and type == 'file'
|
ide.editorManager.openDoc(entity)
|
||||||
ide.binaryFilesManager.openFile(entity)
|
else if type == 'file'
|
||||||
else
|
ide.binaryFilesManager.openFile(entity)
|
||||||
setTimeout(tryOpen, 500)
|
.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"
|
||||||
|
], () ->
|
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
|
|
@ -27,6 +27,122 @@ describe "LinkedFiles", ->
|
||||||
@owner.login ->
|
@owner.login ->
|
||||||
mkdirp Settings.path.dumpFolder, done
|
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", ->
|
describe "creating a URL based linked file", ->
|
||||||
before (done) ->
|
before (done) ->
|
||||||
@owner.createProject "url-linked-files-project", {template: "blank"}, (error, project_id) =>
|
@owner.createProject "url-linked-files-project", {template: "blank"}, (error, project_id) =>
|
||||||
|
@ -50,7 +166,7 @@ describe "LinkedFiles", ->
|
||||||
name: 'url-test-file-1'
|
name: 'url-test-file-1'
|
||||||
}, (error, response, body) =>
|
}, (error, response, body) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
expect(response.statusCode).to.equal 204
|
expect(response.statusCode).to.equal 200
|
||||||
@owner.getProject @project_id, (error, project) =>
|
@owner.getProject @project_id, (error, project) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
file = project.rootFolder[0].fileRefs[0]
|
file = project.rootFolder[0].fileRefs[0]
|
||||||
|
@ -76,7 +192,7 @@ describe "LinkedFiles", ->
|
||||||
name: 'url-test-file-2'
|
name: 'url-test-file-2'
|
||||||
}, (error, response, body) =>
|
}, (error, response, body) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
expect(response.statusCode).to.equal 204
|
expect(response.statusCode).to.equal 200
|
||||||
@owner.request.post {
|
@owner.request.post {
|
||||||
url: "/project/#{@project_id}/linked_file",
|
url: "/project/#{@project_id}/linked_file",
|
||||||
json:
|
json:
|
||||||
|
@ -88,7 +204,7 @@ describe "LinkedFiles", ->
|
||||||
name: 'url-test-file-2'
|
name: 'url-test-file-2'
|
||||||
}, (error, response, body) =>
|
}, (error, response, body) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
expect(response.statusCode).to.equal 204
|
expect(response.statusCode).to.equal 200
|
||||||
@owner.getProject @project_id, (error, project) =>
|
@owner.getProject @project_id, (error, project) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
file = project.rootFolder[0].fileRefs[1]
|
file = project.rootFolder[0].fileRefs[1]
|
||||||
|
@ -168,7 +284,7 @@ describe "LinkedFiles", ->
|
||||||
name: 'url-test-file-6'
|
name: 'url-test-file-6'
|
||||||
}, (error, response, body) =>
|
}, (error, response, body) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
expect(response.statusCode).to.equal 204
|
expect(response.statusCode).to.equal 200
|
||||||
@owner.getProject @project_id, (error, project) =>
|
@owner.getProject @project_id, (error, project) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
file = _.find project.rootFolder[0].fileRefs, (file) ->
|
file = _.find project.rootFolder[0].fileRefs, (file) ->
|
||||||
|
|
|
@ -143,6 +143,18 @@ class User
|
||||||
return callback(err)
|
return callback(err)
|
||||||
callback(null)
|
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) ->) ->
|
addUserToProject: (project_id, user, privileges, callback = (error, user) ->) ->
|
||||||
if privileges == 'readAndWrite'
|
if privileges == 'readAndWrite'
|
||||||
updateOp = {$addToSet: {collaberator_refs: user._id.toString()}}
|
updateOp = {$addToSet: {collaberator_refs: user._id.toString()}}
|
||||||
|
|
|
@ -67,6 +67,7 @@ describe "ProjectController", ->
|
||||||
protectTokens: sinon.stub()
|
protectTokens: sinon.stub()
|
||||||
@CollaboratorsHandler =
|
@CollaboratorsHandler =
|
||||||
userIsTokenMember: sinon.stub().callsArgWith(2, null, false)
|
userIsTokenMember: sinon.stub().callsArgWith(2, null, false)
|
||||||
|
@ProjectEntityHandler = {}
|
||||||
@Modules =
|
@Modules =
|
||||||
hooks:
|
hooks:
|
||||||
fire: sinon.stub()
|
fire: sinon.stub()
|
||||||
|
@ -98,6 +99,7 @@ describe "ProjectController", ->
|
||||||
"../TokenAccess/TokenAccessHandler": @TokenAccessHandler
|
"../TokenAccess/TokenAccessHandler": @TokenAccessHandler
|
||||||
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
|
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
|
||||||
"../../infrastructure/Modules": @Modules
|
"../../infrastructure/Modules": @Modules
|
||||||
|
"./ProjectEntityHandler": @ProjectEntityHandler
|
||||||
|
|
||||||
@projectName = "£12321jkj9ujkljds"
|
@projectName = "£12321jkj9ujkljds"
|
||||||
@req =
|
@req =
|
||||||
|
@ -520,7 +522,62 @@ describe "ProjectController", ->
|
||||||
@ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true
|
@ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true
|
||||||
done()
|
done()
|
||||||
@ProjectController.loadEditor @req, @res
|
@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', ->
|
describe '_isInPercentageRollout', ->
|
||||||
before ->
|
before ->
|
||||||
@ids = [
|
@ids = [
|
||||||
|
|
Loading…
Reference in a new issue