Merge pull request #579 from sharelatex/sk-linked-files-from-project

Linked Files from Project
This commit is contained in:
Shane Kilkelly 2018-05-31 11:08:08 +01:00 committed by GitHub
commit d3ae276091
20 changed files with 703 additions and 43 deletions

View file

@ -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
}

View file

@ -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()

View file

@ -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)}"
}
}

View file

@ -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()

View file

@ -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)

View file

@ -119,6 +119,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

View file

@ -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}}

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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 = () ->) ->

View file

@ -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)

View file

@ -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) ->

View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,3 @@
define [
"ide/test-controls/controllers/TestControlsController"
], () ->

View 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

View file

@ -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) ->

View 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()}}

View file

@ -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 = [