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 = { 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
}

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

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

View file

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