Merge branch 'master' into pr-v2-history-ui

This commit is contained in:
Paulo Reis 2018-06-05 10:55:42 +01:00
commit 8e5032fb34
39 changed files with 1060 additions and 105 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

@ -45,37 +45,40 @@ wrapWithLock = (methodWithoutLock) ->
methodWithLock
module.exports = ProjectEntityUpdateHandler = self =
# this doesn't need any locking because it's only called by ProjectDuplicator
copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
project_id = project._id
projectHistoryId = project.overleaf?.history?.id
logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
return callback(err) if err?
ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id)=>
if !origonalFileRef?
logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
return callback()
# convert any invalid characters in original file to '_'
fileRef = new File name : SafePath.clean(origonalFileRef.name)
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
if err?
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
return callback(err)
ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
if err?
logger.err { err, project_id, folder_id }, "error putting element as part of copy"
return callback(err)
TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
copyFileFromExistingProjectWithProject: wrapWithLock
beforeLock: (next) ->
(project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
project_id = project._id
logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id) ->
if !origonalFileRef?
logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
return callback()
# convert any invalid characters in original file to '_'
fileRef = new File name : SafePath.clean(origonalFileRef.name)
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
if err?
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
newFiles = [
file: fileRef
path: result?.path?.fileSystem
url: fileStoreUrl
]
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles}, (error) ->
return callback(error) if error?
callback null, fileRef, folder_id
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
return callback(err)
next(project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback)
withLock: (project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)->
project_id = project._id
projectHistoryId = project.overleaf?.history?.id
ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result, newProject) ->
if err?
logger.err { err, project_id, folder_id }, "error putting element as part of copy"
return callback(err)
TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
if err?
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
newFiles = [
file: fileRef
path: result?.path?.fileSystem
url: fileStoreUrl
]
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles, newProject}, (error) ->
return callback(error) if error?
callback null, fileRef, folder_id
updateDocLines: (project_id, doc_id, lines, version, ranges, callback = (error) ->)->
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->

View file

@ -10,6 +10,8 @@ Async = require('async')
oneMinInMs = 60 * 1000
fiveMinsInMs = oneMinInMs * 5
if !settings.apis?.references?.url?
logger.log "references search not enabled"
module.exports = ReferencesHandler =

View file

@ -0,0 +1,80 @@
path = require('path')
Project = require('../../../js/models/Project').Project
ProjectUploadManager = require('../../../js/Features/Uploads/ProjectUploadManager')
ProjectOptionsHandler = require('../../../js/Features/Project/ProjectOptionsHandler')
AuthenticationController = require('../../../js/Features/Authentication/AuthenticationController')
settings = require('settings-sharelatex')
fs = require('fs')
request = require('request')
uuid = require('uuid')
logger = require('logger-sharelatex')
async = require("async")
module.exports = TemplatesController =
getV1Template: (req, res)->
templateVersionId = req.params.Template_version_id
templateId = req.query.id
if !/^[0-9]+$/.test(templateVersionId) || !/^[0-9]+$/.test(templateId)
logger.err templateVersionId:templateVersionId, templateId: templateId, "invalid template id or version"
return res.sendStatus 400
data = {}
data.templateVersionId = templateVersionId
data.templateId = templateId
data.name = req.query.templateName
data.compiler = req.query.latexEngine
res.render path.resolve(__dirname, "../../../views/project/editor/new_from_template"), data
createProjectFromV1Template: (req, res)->
currentUserId = AuthenticationController.getLoggedInUserId(req)
zipUrl = "#{settings.apis.v1.url}/api/v1/sharelatex/templates/#{req.body.templateVersionId}"
zipReq = request(zipUrl, {
'auth': {
'user': settings.apis.v1.user,
'pass': settings.apis.v1.pass
}
})
TemplatesController.createFromZip(
zipReq,
{
templateName: req.body.templateName,
currentUserId: currentUserId,
compiler: req.body.compiler
docId: req.body.docId
templateId: req.body.templateId
templateVersionId: req.body.templateVersionId
},
req,
res
)
createFromZip: (zipReq, options, req, res)->
dumpPath = "#{settings.path.dumpFolder}/#{uuid.v4()}"
writeStream = fs.createWriteStream(dumpPath)
zipReq.on "error", (error) ->
logger.error err: error, "error getting zip from template API"
zipReq.pipe(writeStream)
writeStream.on 'close', ->
ProjectUploadManager.createProjectFromZipArchive options.currentUserId, options.templateName, dumpPath, (err, project)->
if err?
logger.err err:err, zipReq:zipReq, "problem building project from zip"
return res.sendStatus 500
setCompiler project._id, options.compiler, ->
fs.unlink dumpPath, ->
delete req.session.templateData
conditions = {_id:project._id}
update = {
fromV1TemplateId:options.templateId,
fromV1TemplateVersionId:options.templateVersionId
}
Project.update conditions, update, {}, (err)->
res.redirect "/project/#{project._id}"
setCompiler = (project_id, compiler, callback)->
if compiler?
ProjectOptionsHandler.setCompiler project_id, compiler, callback
else
callback()

View file

@ -0,0 +1,8 @@
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
module.exports =
saveTemplateDataInSession: (req, res, next)->
if req.query.templateName
req.session.templateData = req.query
next()

View file

@ -0,0 +1,9 @@
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
module.exports =
saveTemplateDataInSession: (req, res, next)->
if req.query.templateName
req.session.templateData = req.query
next()

View file

@ -0,0 +1,10 @@
AuthenticationController = require('../Authentication/AuthenticationController')
TemplatesController = require("./TemplatesController")
TemplatesMiddlewear = require('./TemplatesMiddlewear')
module.exports =
apply: (app)->
app.get '/project/new/template/:Template_version_id', TemplatesMiddlewear.saveTemplateDataInSession, AuthenticationController.requireLogin(), TemplatesController.getV1Template
app.post '/project/new/template', AuthenticationController.requireLogin(), TemplatesController.createProjectFromV1Template

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

@ -48,6 +48,7 @@ MetaController = require('./Features/Metadata/MetaController')
TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
Features = require('./infrastructure/Features')
LinkedFilesRouter = require './Features/LinkedFiles/LinkedFilesRouter'
TemplatesRouter = require './Features/Templates/TemplatesRouter'
logger = require("logger-sharelatex")
_ = require("underscore")
@ -80,10 +81,10 @@ module.exports = class Router
ContactRouter.apply(webRouter, privateApiRouter)
AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter)
LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter)
TemplatesRouter.apply(webRouter)
Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
if Settings.enableSubscriptions
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus
@ -119,6 +120,11 @@ module.exports = class Router
webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.userProjectsJson
webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanReadProject,
ProjectController.projectEntitiesJson
webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage
webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
@ -202,7 +208,7 @@ module.exports = class Router
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
webRouter.get "/project/:Project_id/filetree/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory

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

@ -33,11 +33,11 @@ div.full-size(
i.fa.fa-arrow-left
|   #{translate("open_a_file_on_the_left")}
!= moduleIncludes('editor:toolbar', locals)
!= moduleIncludes('editor:main', locals)
#editor(
ace-editor="editor",
ng-if="!editor.richText",
ng-if="!editor.showRichText",
ng-show="!!editor.sharejs_doc && !editor.opening",
style=showRichText ? "top: 32px" : "",
theme="settings.theme",
@ -73,8 +73,6 @@ div.full-size(
line-height="settings.lineHeight || ui.defaultLineHeight"
)
!= moduleIncludes('editor:body', locals)
include ./review-panel
.ui-layout-east

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

@ -13,7 +13,7 @@
}"
)
| in <strong>{{history.diff.pathname}}</strong>
.toolbar-right
.toolbar-right(ng-if="permissions.write")
a.btn.btn-danger.btn-sm(
href,
ng-click="openRestoreDiffModal()"

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

@ -0,0 +1,26 @@
extends ../../layout
block content
script.
$(document).ready(function(){
$('#create_form').submit();
});
.editor.full-size
.loading-screen()
.loading-screen-brand-container
.loading-screen-brand(
style="height: 20%;"
)
h3.loading-screen-label() #{translate("Opening template")}
span.loading-screen-ellip .
span.loading-screen-ellip .
span.loading-screen-ellip .
form(id='create_form' method='POST' action='/project/new/template/')
input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden" name="templateId" value=templateId)
input(type="hidden" name="templateVersionId" value=templateVersionId)
input(type="hidden" name="templateName" value=name)
input(type="hidden" name="compiler" value=compiler)

View file

@ -1,4 +1,7 @@
.col-xs-6
- var titleClasses = settings.overleaf ? "col-xs-6 col-sm-4 col-md-6" : "col-xs-6"
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4"
div(class=titleClasses)
input.select-item(
select-individual,
type="checkbox",
@ -37,8 +40,42 @@
tooltip-placement="right"
tooltip-append-to-body="true"
)
.col-xs-4
div(class=lastUpdatedClasses)
if settings.overleaf
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}
else
span.last-modified {{project.lastUpdated | formatDate}}
if settings.overleaf
.hidden-xs.col-sm-3.col-md-2.action-btn-row
button.btn.btn-link.action-btn(
tooltip=translate('copy'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="clone($event)"
)
i.icon.fa.fa-files-o
button.btn.btn-link.action-btn(
tooltip=translate('download'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="download($event)"
)
i.icon.fa.fa-cloud-download
button.btn.btn-link.action-btn(
ng-if="!project.archived"
tooltip=translate('archive'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="archive($event)"
)
i.icon.fa.fa-inbox
button.btn.btn-link.action-btn(
ng-if="project.archived"
tooltip=translate('unarchive'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="restore($event)"
)
i.icon.fa.fa-reply

View file

@ -131,7 +131,10 @@
)
li.container-fluid
.row
.col-xs-6
- var titleClasses = settings.overleaf ? " col-xs-6 col-sm-4 col-md-6" : "col-xs-6"
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4"
div(class=titleClasses)
input.select-all(
select-all,
type="checkbox"
@ -142,9 +145,12 @@
.col-xs-2
span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")}
i.tablesort.fa(ng-class="getSortIconClass('accessLevel')")
.col-xs-4
div(class=lastUpdatedClasses)
span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")}
i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')")
if settings.overleaf
.hidden-xs.col-sm-3.col-md-2.action-btn-row-header
span.header #{translate("actions")}
li.project_entry.container-fluid(
ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
ng-controller="ProjectListItemController"

View file

@ -1,4 +1,4 @@
.col-xs-6
.col-xs-6.col-sm-4.col-md-6
.select-item
span.v1-badge(
aria-label=translate("v1_badge")
@ -21,5 +21,5 @@
.col-xs-2
span.owner {{ownerName()}}
.col-xs-4
.col-xs-4.col-sm-3.col-md-2
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}

View file

@ -146,8 +146,8 @@ module.exports = settings =
url: "http://#{process.env['CONTACTS_HOST'] or 'localhost'}:3036"
sixpack:
url: ""
# references:
# url: "http://localhost:3040"
references:
url: "http://#{process.env['REFERENCES_HOST'] or 'localhost'}:3040"
notifications:
url: "http://#{process.env['NOTIFICATIONS_HOST'] or 'localhost'}:3042"
analytics:

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

@ -14,7 +14,7 @@ define [
opening: true
trackChanges: false
wantTrackChanges: false
richText: false
showRichText: false
}
@$scope.$on "entity:selected", (event, entity) =>

View file

@ -335,6 +335,11 @@ define [
return null
projectContainsFolder: () ->
for entity in @$scope.rootFolder.children
return true if entity.type == 'folder'
return false
existsInThisFolder: (folder, name) ->
for entity in folder?.children or []
return true if entity.name is name

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

@ -350,14 +350,15 @@ define [
$scope.archiveOrLeaveSelectedProjects()
$scope.archiveOrLeaveSelectedProjects = () ->
selected_projects = $scope.getSelectedProjects()
selected_project_ids = $scope.getSelectedProjectIds()
$scope.archiveOrLeaveProjects($scope.getSelectedProjects())
$scope.archiveOrLeaveProjects = (projects) ->
projectIds = projects.map (p) -> p.id
# Remove project from any tags
for tag in $scope.tags
$scope._removeProjectIdsFromTagArray(tag, selected_project_ids)
$scope._removeProjectIdsFromTagArray(tag, projectIds)
for project in selected_projects
for project in projects
project.tags = []
if project.accessLevel == "owner"
project.archived = true
@ -414,16 +415,17 @@ define [
$scope.updateVisibleProjects()
$scope.restoreSelectedProjects = () ->
selected_projects = $scope.getSelectedProjects()
selected_project_ids = $scope.getSelectedProjectIds()
$scope.restoreProjects($scope.getSelectedProjects())
for project in selected_projects
$scope.restoreProjects = (projects) ->
projectIds = projects.map (p) -> p.id
for project in projects
project.archived = false
for project_id in selected_project_ids
for projectId in projectIds
queuedHttp {
method: "POST"
url: "/project/#{project_id}/restore"
url: "/project/#{projectId}/restore"
headers:
"X-CSRF-Token": window.csrfToken
}
@ -437,13 +439,14 @@ define [
)
$scope.downloadSelectedProjects = () ->
selected_project_ids = $scope.getSelectedProjectIds()
event_tracking.send 'project-list-page-interaction', 'project action', 'Download Zip'
if selected_project_ids.length > 1
path = "/project/download/zip?project_ids=#{selected_project_ids.join(',')}"
else
path = "/project/#{selected_project_ids[0]}/download/zip"
$scope.downloadProjectsById($scope.getSelectedProjectIds())
$scope.downloadProjectsById = (projectIds) ->
event_tracking.send 'project-list-page-interaction', 'project action', 'Download Zip'
if projectIds.length > 1
path = "/project/download/zip?project_ids=#{projectIds.join(',')}"
else
path = "/project/#{projectIds[0]}/download/zip"
window.location = path
$scope.openV1ImportModal = (project) ->
@ -490,3 +493,19 @@ define [
$scope.$watch "project.selected", (value) ->
if value?
$scope.updateSelectedProjects()
$scope.clone = (e) ->
e.stopPropagation()
$scope.cloneProject($scope.project, "#{$scope.project.name} (Copy)")
$scope.download = (e) ->
e.stopPropagation()
$scope.downloadProjectsById([$scope.project.id])
$scope.archive = (e) ->
e.stopPropagation()
$scope.archiveOrLeaveProjects([$scope.project])
$scope.restore = (e) ->
e.stopPropagation()
$scope.restoreProjects([$scope.project])

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

@ -369,6 +369,16 @@ ul.project-list {
.v1-badge {
margin-left: -4px;
}
.action-btn-row-header, .action-btn-row {
padding-right: 20px;
text-align: right;
}
.action-btn {
padding: 0 0.3em;
margin-left: 0.2em;
}
}
i.tablesort {
padding-left: 8px;

View file

@ -145,10 +145,15 @@ output {
opacity: 1; // iOS fix for unreadable disabled content
}
// Reset height for `textarea`s
// Reset height for `textarea`s, and smaller border-radius
textarea& {
height: auto;
border-radius: @border-radius-base;
}
// Smaller border-radius for `select` inputs
select& {
border-radius: @border-radius-base;
}
}

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

View file

@ -0,0 +1,76 @@
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = '../../../../app/js/Features/Templates/TemplatesController'
describe 'TemplatesController', ->
project_id = "213432"
beforeEach ->
@request = sinon.stub()
@request.returns {
pipe:->
on:->
}
@fs = {
unlink : sinon.stub()
createWriteStream : sinon.stub().returns(on:(_, cb)->cb())
}
@ProjectUploadManager = {createProjectFromZipArchive : sinon.stub().callsArgWith(3, null, {_id:project_id})}
@dumpFolder = "dump/path"
@ProjectOptionsHandler = {setCompiler:sinon.stub().callsArgWith(2)}
@uuid = "1234"
@ProjectDetailsHandler =
getProjectDescription:sinon.stub()
@Project =
update: sinon.stub().callsArgWith(3, null)
@controller = SandboxedModule.require modulePath, requires:
'../../../js/Features/Uploads/ProjectUploadManager':@ProjectUploadManager
'../../../js/Features/Project/ProjectOptionsHandler':@ProjectOptionsHandler
'../../../js/Features/Authentication/AuthenticationController': @AuthenticationController = {getLoggedInUserId: sinon.stub()}
'./TemplatesPublisher':@TemplatesPublisher
"logger-sharelatex":
log:->
err:->
"settings-sharelatex":
path:
dumpFolder:@dumpFolder
siteUrl: @siteUrl = "http://localhost:3000"
apis:
v1:
url: @v1Url="http://overleaf.com"
user: "sharelatex"
pass: "password"
overleaf:
host: @v1Url
"uuid":v4:=>@uuid
"request": @request
"fs":@fs
"../../../../app/js/models/Project": {Project: @Project}
@zipUrl = "%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex"
@templateName = "project name here"
@user_id = "1234"
@req =
session:
user: _id:@user_id
templateData:
zipUrl: @zipUrl
templateName: @templateName
@redirect = {}
@AuthenticationController.getLoggedInUserId.returns(@user_id)
describe 'v1Templates', ->
it "should fetch zip from v1 based on template id", (done)->
@templateVersionId = 15
@req.body = {templateVersionId: @templateVersionId}
redirect = =>
@request.calledWith("#{@v1Url}/api/v1/sharelatex/templates/#{@templateVersionId}").should.equal true
done()
res = redirect:redirect
@controller.createProjectFromV1Template @req, res