diff --git a/services/web/app/coffee/Features/Compile/CompileManager.coffee b/services/web/app/coffee/Features/Compile/CompileManager.coffee index 4f4fe89e94..515369fef8 100755 --- a/services/web/app/coffee/Features/Compile/CompileManager.coffee +++ b/services/web/app/coffee/Features/Compile/CompileManager.coffee @@ -28,8 +28,8 @@ module.exports = CompileManager = CompileManager._checkIfAutoCompileLimitHasBeenHit options.isAutoCompile, "everyone", (err, canCompile)-> if !canCompile return callback null, "autocompile-backoff", [] - - CompileManager._ensureRootDocumentIsSet project_id, (error) -> + + ProjectRootDocManager.ensureRootDocumentIsSet project_id, (error) -> return callback(error) if error? CompileManager.getProjectCompileLimits project_id, (error, limits) -> return callback(error) if error? @@ -103,17 +103,6 @@ module.exports = CompileManager = Metrics.inc "auto-compile-#{compileGroup}-limited" callback err, canCompile - _ensureRootDocumentIsSet: (project_id, callback = (error) ->) -> - ProjectGetter.getProject project_id, rootDoc_id: 1, (error, project) -> - return callback(error) if error? - if !project? - return callback new Error("project not found") - - if project.rootDoc_id? - callback() - else - ProjectRootDocManager.setRootDocAutomatically project_id, callback - wordCount: (project_id, user_id, file, callback = (error) ->) -> CompileManager.getProjectCompileLimits project_id, (error, limits) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/Exports/ExportsController.coffee b/services/web/app/coffee/Features/Exports/ExportsController.coffee index fc339f9957..2e3ed18bdb 100644 --- a/services/web/app/coffee/Features/Exports/ExportsController.coffee +++ b/services/web/app/coffee/Features/Exports/ExportsController.coffee @@ -4,7 +4,7 @@ logger = require("logger-sharelatex") module.exports = - exportProject: (req, res) -> + exportProject: (req, res, next) -> {project_id, brand_variation_id} = req.params user_id = AuthenticationController.getLoggedInUserId(req) export_params = { @@ -21,10 +21,10 @@ module.exports = export_params.description = req.body.description.trim() if req.body.description export_params.author = req.body.author.trim() if req.body.author export_params.license = req.body.license.trim() if req.body.license - export_params.show_source = req.body.show_source if req.body.show_source + export_params.show_source = req.body.showSource if req.body.showSource? ExportsHandler.exportProject export_params, (err, export_data) -> - return err if err? + return next(err) if err? logger.log user_id:user_id project_id: project_id @@ -36,7 +36,13 @@ module.exports = exportStatus: (req, res) -> {export_id} = req.params ExportsHandler.fetchExport export_id, (err, export_json) -> - return err if err? + if err? + json = { + status_summary: 'failed', + status_detail: err.toString, + } + res.send export_json: json + return err parsed_export = JSON.parse(export_json) json = { status_summary: parsed_export.status_summary, @@ -46,11 +52,11 @@ module.exports = } res.send export_json: json - exportDownload: (req, res) -> + exportDownload: (req, res, next) -> {type, export_id} = req.params AuthenticationController.getLoggedInUserId(req) ExportsHandler.fetchDownload export_id, type, (err, export_file_url) -> - return err if err? + return next(err) if err? res.redirect export_file_url diff --git a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee index 18644a66e6..75bd3ce270 100644 --- a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee +++ b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee @@ -1,5 +1,6 @@ ProjectGetter = require('../Project/ProjectGetter') ProjectLocator = require('../Project/ProjectLocator') +ProjectRootDocManager = require('../Project/ProjectRootDocManager') UserGetter = require('../User/UserGetter') logger = require('logger-sharelatex') settings = require 'settings-sharelatex' @@ -20,13 +21,16 @@ module.exports = ExportsHandler = self = callback null, export_data _buildExport: (export_params, callback=(err, export_data) ->) -> - {project_id, user_id, brand_variation_id, title, description, author, license, show_source} = export_params + {project_id, user_id, brand_variation_id, title, description, author, + license, show_source} = export_params jobs = project: (cb) -> ProjectGetter.getProject project_id, cb # TODO: when we update async, signature will change from (cb, results) to (results, cb) rootDoc: [ 'project', (cb, results) -> - ProjectLocator.findRootDoc {project: results.project, project_id: project_id}, cb + ProjectRootDocManager.ensureRootDocumentIsSet project_id, (error) -> + return callback(error) if error? + ProjectLocator.findRootDoc {project: results.project, project_id: project_id}, cb ] user: (cb) -> UserGetter.getUser user_id, {first_name: 1, last_name: 1, email: 1, overleaf: 1}, cb @@ -62,7 +66,7 @@ module.exports = ExportsHandler = self = description: description author: author license: license - show_source: show_source + showSource: show_source user: id: user_id firstName: user.first_name diff --git a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee index 973d2b2ed6..8474573be2 100644 --- a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee +++ b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee @@ -1,5 +1,6 @@ ProjectEntityHandler = require "./ProjectEntityHandler" ProjectEntityUpdateHandler = require "./ProjectEntityUpdateHandler" +ProjectGetter = require "./ProjectGetter" Path = require "path" async = require("async") _ = require("underscore") @@ -45,13 +46,24 @@ module.exports = ProjectRootDocManager = # docpaths have a leading / so allow matching "folder/filename" and "/folder/filename" if path == rootDocName root_doc_id = doc_id - # try a basename match if there was no match + # try a basename match if there was no match if !root_doc_id for doc_id, path of docPaths - if Path.basename(path) == Path.basename(rootDocName) + if Path.basename(path) == Path.basename(rootDocName) root_doc_id = doc_id # set the root doc id if we found a match if root_doc_id? ProjectEntityUpdateHandler.setRootDoc project_id, root_doc_id, callback else - callback() \ No newline at end of file + callback() + + ensureRootDocumentIsSet: (project_id, callback = (error) ->) -> + ProjectGetter.getProject project_id, rootDoc_id: 1, (error, project) -> + return callback(error) if error? + if !project? + return callback new Error("project not found") + + if project.rootDoc_id? + callback() + else + ProjectRootDocManager.setRootDocAutomatically project_id, callback diff --git a/services/web/public/stylesheets/app/editor/publish-modal.less b/services/web/public/stylesheets/app/editor/publish-modal.less index dbf49a945d..58c5a776b4 100644 --- a/services/web/public/stylesheets/app/editor/publish-modal.less +++ b/services/web/public/stylesheets/app/editor/publish-modal.less @@ -1,12 +1,17 @@ .modal-body-publish { + @label-column-width: 10em; + @control-left-margin: 1.0em; .form-control-box { margin-bottom: 1.5ex; - margin-left: 1.0em; + margin-left: @control-left-margin; label { display: inline-block; - width: 10em; + width: @label-column-width; vertical-align: baseline; } + label.checkbox-label { + width: auto; + } .form-control { display: inline-block; width: 60%; @@ -26,6 +31,12 @@ option { margin-left: -4px; } + a.help { + margin-left: 0.6em; + } + } + .no-label { + margin-left: @label-column-width + @control-left-margin; } #search-input-container { overflow: hidden; diff --git a/services/web/test/acceptance/coffee/ExportsTests.coffee b/services/web/test/acceptance/coffee/ExportsTests.coffee index 95e7b2984c..790627776c 100644 --- a/services/web/test/acceptance/coffee/ExportsTests.coffee +++ b/services/web/test/acceptance/coffee/ExportsTests.coffee @@ -36,7 +36,7 @@ describe 'Exports', -> description: 'description' author: 'author' license: 'other' - show_source: true + showSource: true }, (error, response, body) => throw error if error? expect(response.statusCode).to.equal 200 @@ -53,7 +53,7 @@ describe 'Exports', -> expect(project.metadata.description).to.equal 'description' expect(project.metadata.author).to.equal 'author' expect(project.metadata.license).to.equal 'other' - expect(project.metadata.show_source).to.equal true + expect(project.metadata.showSource).to.equal true # version should match what was retrieved from project-history expect(project.historyVersion).to.equal @version # user details should match diff --git a/services/web/test/unit/coffee/Compile/CompileManagerTests.coffee b/services/web/test/unit/coffee/Compile/CompileManagerTests.coffee index 46dc88be70..890a02ac24 100644 --- a/services/web/test/unit/coffee/Compile/CompileManagerTests.coffee +++ b/services/web/test/unit/coffee/Compile/CompileManagerTests.coffee @@ -10,7 +10,7 @@ describe "CompileManager", -> beforeEach -> @rateLimitGetStub = sinon.stub() rateLimitGetStub = @rateLimitGetStub - @ratelimiter = + @ratelimiter = addCount: sinon.stub() @CompileManager = SandboxedModule.require modulePath, requires: "settings-sharelatex": @settings = @@ -34,11 +34,11 @@ describe "CompileManager", -> timeout: 42 } - + describe "compile", -> beforeEach -> @CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, false) - @CompileManager._ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null) + @ProjectRootDocManager.ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null) @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, @limits) @ClsiManager.sendRequest = sinon.stub().callsArgWith(3, null, @status = "mock-status", @outputFiles = "mock output files", @output = "mock output") @@ -53,7 +53,7 @@ describe "CompileManager", -> .should.equal true it "should ensure that the root document is set", -> - @CompileManager._ensureRootDocumentIsSet + @ProjectRootDocManager.ensureRootDocumentIsSet .calledWith(@project_id) .should.equal true @@ -81,7 +81,7 @@ describe "CompileManager", -> @logger.log .calledWith(project_id: @project_id, user_id: @user_id, "compiling project") .should.equal true - + describe "when the project has been recently compiled", -> it "should return", (done)-> @CompileManager._checkIfAutoCompileLimitHasBeenHit = (isAutoCompile, compileGroup, cb)-> cb(null, true) @@ -96,7 +96,7 @@ describe "CompileManager", -> @CompileManager.compile @project_id, @user_id, {}, (err, status)-> status.should.equal "autocompile-backoff" done() - + describe "getProjectCompileLimits", -> beforeEach -> @features = { @@ -106,17 +106,17 @@ describe "CompileManager", -> @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project = { owner_ref: @owner_id = "owner-id-123" }) @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { features: @features }) @CompileManager.getProjectCompileLimits @project_id, @callback - + it "should look up the owner of the project", -> @ProjectGetter.getProject .calledWith(@project_id, { owner_ref: 1 }) .should.equal true - + it "should look up the owner's features", -> @UserGetter.getUser .calledWith(@project.owner_ref, { features: 1 }) .should.equal true - + it "should return the limits", -> @callback .calledWith(null, { @@ -124,23 +124,23 @@ describe "CompileManager", -> compileGroup: @group }) .should.equal true - + describe "deleteAuxFiles", -> beforeEach -> @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith 1, null, @limits = { compileGroup: "mock-compile-group" } @ClsiManager.deleteAuxFiles = sinon.stub().callsArg(3) @CompileManager.deleteAuxFiles @project_id, @user_id, @callback - + it "should look up the compile group to use", -> @CompileManager.getProjectCompileLimits .calledWith(@project_id) .should.equal true - + it "should delete the aux files", -> @ClsiManager.deleteAuxFiles .calledWith(@project_id, @user_id, @limits) .should.equal true - + it "should call the callback", -> @callback.called.should.equal true @@ -170,55 +170,7 @@ describe "CompileManager", -> it "should call the callback with false", -> @callback.calledWith(null, false).should.equal true - - describe "_ensureRootDocumentIsSet", -> - beforeEach -> - @project = {} - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - @ProjectRootDocManager.setRootDocAutomatically = sinon.stub().callsArgWith(1, null) - - describe "when the root doc is set", -> - beforeEach -> - @project.rootDoc_id = "root-doc-id" - @CompileManager._ensureRootDocumentIsSet(@project_id, @callback) - it "should find the project with only the rootDoc_id fiel", -> - @ProjectGetter.getProject - .calledWith(@project_id, rootDoc_id: 1) - .should.equal true - - it "should not try to update the project rootDoc_id", -> - @ProjectRootDocManager.setRootDocAutomatically - .called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the root doc is not set", -> - beforeEach -> - @CompileManager._ensureRootDocumentIsSet(@project_id, @callback) - - it "should find the project with only the rootDoc_id fiel", -> - @ProjectGetter.getProject - .calledWith(@project_id, rootDoc_id: 1) - .should.equal true - - it "should update the project rootDoc_id", -> - @ProjectRootDocManager.setRootDocAutomatically - .calledWith(@project_id) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the project does not exist", -> - beforeEach -> - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) - @CompileManager._ensureRootDocumentIsSet(@project_id, @callback) - - it "should call the callback with an error", -> - @callback.calledWith(new Error("project not found")).should.equal true - describe "_checkIfAutoCompileLimitHasBeenHit", -> it "should be able to compile if it is not an autocompile", (done)-> @@ -255,16 +207,16 @@ describe "CompileManager", -> @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith 1, null, @limits = { compileGroup: "mock-compile-group" } @ClsiManager.wordCount = sinon.stub().callsArg(4) @CompileManager.wordCount @project_id, @user_id, false, @callback - + it "should look up the compile group to use", -> @CompileManager.getProjectCompileLimits .calledWith(@project_id) .should.equal true - + it "should call wordCount for project", -> @ClsiManager.wordCount .calledWith(@project_id, @user_id, false, @limits) .should.equal true - + it "should call the callback", -> @callback.called.should.equal true diff --git a/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee index 36ff01c3ef..80d2066dcf 100644 --- a/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee +++ b/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee @@ -62,7 +62,7 @@ describe 'ExportsController', -> @req.body.description = description @req.body.author = author @req.body.license = license - @req.body.show_source = true + @req.body.showSource = true it 'should ask the handler to perform the export', (done) -> @handler.exportProject = sinon.stub().yields(null, {iAmAnExport: true, v1_id: 897}) diff --git a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee index f766feb6dd..4952198c3b 100644 --- a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee +++ b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee @@ -8,20 +8,17 @@ SandboxedModule = require('sandboxed-module') describe 'ExportsHandler', -> beforeEach -> - @ProjectGetter = {} - @ProjectLocator = {} - @UserGetter = {} - @settings = {} @stubRequest = {} @request = defaults: => return @stubRequest @ExportsHandler = SandboxedModule.require modulePath, requires: 'logger-sharelatex': log: -> err: -> - '../Project/ProjectGetter': @ProjectGetter - '../Project/ProjectLocator': @ProjectLocator - '../User/UserGetter': @UserGetter - 'settings-sharelatex': @settings + '../Project/ProjectGetter': @ProjectGetter = {} + '../Project/ProjectLocator': @ProjectLocator = {} + '../Project/ProjectRootDocManager': @ProjectRootDocManager = {} + '../User/UserGetter': @UserGetter = {} + 'settings-sharelatex': @settings = {} 'request': @request @project_id = "project-id-123" @project_history_id = 987 @@ -45,34 +42,49 @@ describe 'ExportsHandler', -> @callback = sinon.stub() describe 'exportProject', -> - beforeEach (done) -> + beforeEach -> @export_data = {iAmAnExport: true} @response_body = {iAmAResponseBody: true} @ExportsHandler._buildExport = sinon.stub().yields(null, @export_data) @ExportsHandler._requestExport = sinon.stub().yields(null, @response_body) - @ExportsHandler.exportProject @export_params, (error, export_data) => - @callback(error, export_data) - done() - it "should build the export", -> - @ExportsHandler._buildExport - .calledWith(@export_params) - .should.equal true + describe "when all goes well", -> + beforeEach (done) -> + @ExportsHandler.exportProject @export_params, (error, export_data) => + @callback(error, export_data) + done() - it "should request the export", -> - @ExportsHandler._requestExport - .calledWith(@export_data) - .should.equal true + it "should build the export", -> + @ExportsHandler._buildExport + .calledWith(@export_params) + .should.equal true - it "should return the export", -> - @callback - .calledWith(null, @export_data) - .should.equal true + it "should request the export", -> + @ExportsHandler._requestExport + .calledWith(@export_data) + .should.equal true + + it "should return the export", -> + @callback + .calledWith(null, @export_data) + .should.equal true + + describe "when request can't be built", -> + beforeEach (done) -> + @ExportsHandler._buildExport = sinon.stub().yields(new Error("cannot export project without root doc")) + @ExportsHandler.exportProject @export_params, (error, export_data) => + @callback(error, export_data) + done() + + it "should return an error", -> + (@callback.args[0][0] instanceof Error) + .should.equal true describe '_buildExport', -> beforeEach (done) -> @project = id: @project_id + rootDoc_id: 'doc1_id' compiler: 'pdflatex' imageName: 'mock-image-name' overleaf: @@ -90,6 +102,7 @@ describe 'ExportsHandler', -> @historyVersion = 777 @ProjectGetter.getProject = sinon.stub().yields(null, @project) @ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, {fileSystem: 'main.tex'}]) + @ProjectRootDocManager.ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null) @UserGetter.getUser = sinon.stub().yields(null, @user) @ExportsHandler._requestVersion = sinon.stub().yields(null, @historyVersion) done() @@ -119,7 +132,7 @@ describe 'ExportsHandler', -> description: @description author: @author license: @license - show_source: @show_source + showSource: @show_source user: id: @user_id firstName: @user.first_name @@ -159,7 +172,7 @@ describe 'ExportsHandler', -> description: @description author: @author license: @license - show_source: @show_source + showSource: @show_source user: id: @user_id firstName: @custom_first_name @@ -186,15 +199,58 @@ describe 'ExportsHandler', -> .should.equal true describe "when project has no root doc", -> - beforeEach (done) -> - @ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, null]) - @ExportsHandler._buildExport @export_params, (error, export_data) => - @callback(error, export_data) - done() + describe "when a root doc can be set automatically", -> + beforeEach (done) -> + @project.rootDoc_id = null + @ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, {fileSystem: 'other.tex'}]) + @ExportsHandler._buildExport @export_params, (error, export_data) => + @callback(error, export_data) + done() - it "should return an error", -> - (@callback.args[0][0] instanceof Error) - .should.equal true + it "should set a root doc", -> + @ProjectRootDocManager.ensureRootDocumentIsSet.called + .should.equal true + + it "should return export data", -> + expected_export_data = + project: + id: @project_id + rootDocPath: 'other.tex' + historyId: @project_history_id + historyVersion: @historyVersion + v1ProjectId: @project_history_id + metadata: + compiler: 'pdflatex' + imageName: 'mock-image-name' + title: @title + description: @description + author: @author + license: @license + showSource: @show_source + user: + id: @user_id + firstName: @user.first_name + lastName: @user.last_name + email: @user.email + orcidId: null + v1UserId: 876 + destination: + brandVariationId: @brand_variation_id + options: + callbackUrl: null + @callback.calledWith(null, expected_export_data) + .should.equal true + + describe "when no root doc can be identified", -> + beforeEach (done) -> + @ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, null]) + @ExportsHandler._buildExport @export_params, (error, export_data) => + @callback(error, export_data) + done() + + it "should return an error", -> + (@callback.args[0][0] instanceof Error) + .should.equal true describe "when user is not found", -> beforeEach (done) -> diff --git a/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee b/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee index 47c2973e9f..10717be532 100644 --- a/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee @@ -12,7 +12,8 @@ describe 'ProjectRootDocManager', -> @ProjectRootDocManager = SandboxedModule.require modulePath, requires: "./ProjectEntityHandler" : @ProjectEntityHandler = {} "./ProjectEntityUpdateHandler" : @ProjectEntityUpdateHandler = {} - + "./ProjectGetter" : @ProjectGetter = {} + describe "setRootDocAutomatically", -> describe "when there is a suitable root doc", -> beforeEach (done)-> @@ -165,3 +166,53 @@ describe 'ProjectRootDocManager', -> it "should not set the root doc", -> @ProjectEntityUpdateHandler.setRootDoc.called.should.equal false + + + describe "ensureRootDocumentIsSet", -> + beforeEach -> + @project = {} + @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) + @ProjectRootDocManager.setRootDocAutomatically = sinon.stub().callsArgWith(1, null) + + describe "when the root doc is set", -> + beforeEach -> + @project.rootDoc_id = "root-doc-id" + @ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback) + + it "should find the project with only the rootDoc_id fiel", -> + @ProjectGetter.getProject + .calledWith(@project_id, rootDoc_id: 1) + .should.equal true + + it "should not try to update the project rootDoc_id", -> + @ProjectRootDocManager.setRootDocAutomatically + .called.should.equal false + + it "should call the callback", -> + @callback.called.should.equal true + + describe "when the root doc is not set", -> + beforeEach -> + @ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback) + + it "should find the project with only the rootDoc_id fiel", -> + @ProjectGetter.getProject + .calledWith(@project_id, rootDoc_id: 1) + .should.equal true + + it "should update the project rootDoc_id", -> + @ProjectRootDocManager.setRootDocAutomatically + .calledWith(@project_id) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true + + describe "when the project does not exist", -> + beforeEach -> + @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) + @ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback) + + it "should call the callback with an error", -> + @callback.calledWith(new Error("project not found")).should.equal true +