From a0142d4415430ead4d73a8e8efbab4dd3a34de0f Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 13 Aug 2015 22:40:28 +0100 Subject: [PATCH] added inactive and reactivate project logic --- .../Features/Docstore/DocstoreManager.coffee | 28 ++++++ .../InactiveProjectController.coffee | 11 +++ .../InactiveProjectManager.coffee | 53 ++++++++++++ .../Features/Project/ProjectController.coffee | 6 ++ .../Project/ProjectUpdateHandler.coffee | 22 +++++ services/web/app/coffee/models/Project.coffee | 2 + .../Docstore/DocstoreManagerTests.coffee | 41 ++++++++- .../InactiveProjectManagerTests.coffee | 85 +++++++++++++++++++ .../Project/ProjectControllerTests.coffee | 22 +++++ .../Project/ProjectUpdateHandlerTests.coffee | 35 +++++++- 10 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee create mode 100644 services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee create mode 100644 services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee diff --git a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee index 1e23c1f70b..a755c47422 100644 --- a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee +++ b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee @@ -67,3 +67,31 @@ module.exports = DocstoreManager = error = new Error("docstore api responded with non-success code: #{res.statusCode}") logger.error err: error, project_id: project_id, doc_id: doc_id, "error updating doc in docstore" callback(error) + + archiveProject: (project_id, callback)-> + url = "#{settings.apis.docstore.url}/project/#{project_id}/archive" + logger.log project_id:project_id, "archiving project in docstore" + request.post url, (err, res, docs) -> + if err? + logger.err err:err, project_id:project_id, "error archving project in docstore" + return callback(err) + if 200 <= res.statusCode < 300 + callback() + else + error = new Error("docstore api responded with non-success code: #{res.statusCode}") + logger.err err: error, project_id: project_id, "error archiving project in docstore" + return callback(error) + + unarchiveProject: (project_id, callback)-> + url = "#{settings.apis.docstore.url}/project/#{project_id}/unarchive" + logger.log project_id:project_id, "unarchiving project in docstore" + request.post url, (err, res, docs) -> + if err? + logger.err err:err, project_id:project_id, "error unarchiving project in docstore" + return callback(err) + if 200 <= res.statusCode < 300 + callback() + else + error = new Error("docstore api responded with non-success code: #{res.statusCode}") + logger.err err: error, project_id: project_id, "error unarchiving project in docstore" + return callback(error) \ No newline at end of file diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee new file mode 100644 index 0000000000..b37b43872f --- /dev/null +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee @@ -0,0 +1,11 @@ +InactiveProjectManager = require("./InactiveProjectManager") + +module.exports = + + deactivateOldProjects: (req, res)-> + InactiveProjectManager.deactivateOldProjects 10, (err)-> + if err? + res.sendStatus(500) + else + res.sendStatus(200) + diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee new file mode 100644 index 0000000000..addd6e8806 --- /dev/null +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee @@ -0,0 +1,53 @@ +async = require("async") +_ = require("lodash") +logger = require("logger-sharelatex") +DocstoreManager = require("../Docstore/DocstoreManager") +ProjectGetter = require("../Project/ProjectGetter") +ProjectUpdateHandler = require("../Project/ProjectUpdateHandler") +Project = require("../../models/Project").Project + +MILISECONDS_IN_DAY = 86400000 +module.exports = InactiveProjectManager = + + reactivateProjectIfRequired: (project_id, callback)-> + ProjectGetter.getProject project_id, {inactive:true}, (err, project)-> + if err? + logger.err err:err, project_id:project_id, "error getting project" + return callback(err) + logger.log project_id:project_id, inactive:project.inactive, "seeing if need to reactivate project" + + if !project.inactive + return callback() + + DocstoreManager.unarchiveProject project_id, (err)-> + if err? + logger.err err:err, project_id:project_id, "error reactivating project in docstore" + return callback(err) + ProjectUpdateHandler.markAsActive project_id, callback + + deactivateOldProjects: (limit, callback)-> + + sixMonthsAgo = new Date() - (MILISECONDS_IN_DAY * 1) + Project.find() + .where("lastOpened").lt(sixMonthsAgo) + .where("inactive").ne(true) + .select("_id") + .limit(limit) + .exec (err, projects)-> + if err? + logger.err err:err, "could not get projects for deactivating" + jobs = _.map projects, (project)-> + return (cb)-> + InactiveProjectManager.deactivateProject project._id, cb + logger.log numberOfProjects:projects?.length, "deactivating projects" + async.series jobs, callback + + + deactivateProject: (project_id, callback)-> + logger.log project_id:project_id, "deactivating inactive project" + DocstoreManager.archiveProject project_id, (err)-> + if err? + logger.err err:err, project_id:project_id, "error deactivating project in docstore" + return callback(err) + ProjectUpdateHandler.markAsInactive project_id, callback + diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 55b59cb76f..d447ffc547 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -14,6 +14,8 @@ _ = require("underscore") Settings = require("settings-sharelatex") SecurityManager = require("../../managers/SecurityManager") fs = require "fs" +InactiveProjectManager = require("../InactiveData/InactiveProjectManager") +ProjectUpdateHandler = require("./ProjectUpdateHandler") module.exports = ProjectController = @@ -189,6 +191,10 @@ module.exports = ProjectController = if user_id == 'openUser' return cb() SubscriptionLocator.getUsersSubscription user_id, cb + activate: (cb)-> + InactiveProjectManager.reactivateProjectIfRequired project_id, cb + markOpened: (cb)-> + ProjectUpdateHandler.markOpened project_id, cb }, (err, results)-> if err? logger.err err:err, "error getting details for project page" diff --git a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee index ffabf60800..25466ec898 100644 --- a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee @@ -1,5 +1,6 @@ Project = require('../../models/Project').Project logger = require('logger-sharelatex') +Project = require("../../models/Project").Project module.exports = markAsUpdated : (project_id, callback)-> @@ -8,3 +9,24 @@ module.exports = Project.update conditions, update, {}, (err)-> if callback? callback() + + markAsOpened : (project_id, callback)-> + conditions = {_id:project_id} + update = {lastOpened:Date.now()} + Project.update conditions, update, {}, (err)-> + if callback? + callback() + + markAsInactive: (project_id, callback)-> + conditions = {_id:project_id} + update = {inactive:true} + Project.update conditions, update, {}, (err)-> + if callback? + callback() + + markAsActive: (project_id, callback)-> + conditions = {_id:project_id} + update = { $unset: { inactive: true }} + Project.update conditions, update, {}, (err)-> + if callback? + callback() \ No newline at end of file diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index a658b42080..010d48d9dd 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -17,6 +17,8 @@ DeletedDocSchema = new Schema ProjectSchema = new Schema name : {type:String, default:'new project'} lastUpdated : {type:Date, default: () -> new Date()} + lastOpened : {type:Date} + inactive : { type: Boolean } owner_ref : {type:ObjectId, ref:'User'} collaberator_refs : [ type:ObjectId, ref:'User' ] readOnly_refs : [ type:ObjectId, ref:'User' ] diff --git a/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee b/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee index c9eda7d1df..f32d48fdc1 100644 --- a/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee @@ -13,7 +13,7 @@ describe "DocstoreManager", -> apis: docstore: url: "docstore.sharelatex.com" - "logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()} + "logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub(), err:->} @requestDefaults.calledWith(jar: false).should.equal true @@ -179,3 +179,42 @@ describe "DocstoreManager", -> project_id: @project_id }, "error getting all docs from docstore") .should.equal true + + + describe "archiveProject", -> + describe "with a successful response code", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, null, statusCode: 204) + @DocstoreManager.archiveProject @project_id, @callback + + it "should call the callback", -> + @callback.called.should.equal true + + describe "with a failed response code", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, null, statusCode: 500) + @DocstoreManager.archiveProject @project_id, @callback + + it "should call the callback with an error", -> + @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true + + + + describe "unarchiveProject", -> + describe "with a successful response code", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, null, statusCode: 204) + @DocstoreManager.unarchiveProject @project_id, @callback + + it "should call the callback", -> + @callback.called.should.equal true + + describe "with a failed response code", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, null, statusCode: 500) + @DocstoreManager.unarchiveProject @project_id, @callback + + it "should call the callback with an error", -> + @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true + + diff --git a/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee b/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee new file mode 100644 index 0000000000..96cb4b024c --- /dev/null +++ b/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee @@ -0,0 +1,85 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +path = require('path') +sinon = require('sinon') +modulePath = path.join __dirname, "../../../../app/js/Features/InactiveData/InactiveProjectManager" +expect = require("chai").expect + +describe "InactiveProjectManager", -> + + beforeEach -> + + @settings = {} + @DocstoreManager = + unarchiveProject:sinon.stub() + @ProjectUpdateHandler = + markAsActive:sinon.stub() + markAsInactive:sinon.stub() + @ProjectGetter = + getProject:sinon.stub() + @InactiveProjectManager = SandboxedModule.require modulePath, requires: + "settings-sharelatex":@settings + "logger-sharelatex": + log:-> + err:-> + "../Docstore/DocstoreManager":@DocstoreManager + "../Project/ProjectUpdateHandler":@ProjectUpdateHandler + "../Project/ProjectGetter":@ProjectGetter + + @project_id = "1234" + + describe "reactivateProjectIfRequired", -> + + beforeEach -> + @project = {inactive:true} + @ProjectGetter.getProject.callsArgWith(2, null, @project) + @ProjectUpdateHandler.markAsActive.callsArgWith(1) + + it "should call unarchiveProject", (done)-> + @DocstoreManager.unarchiveProject.callsArgWith(1) + @InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=> + @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal true + done() + + it "should not mark project as active if error with unarchinging", (done)-> + @DocstoreManager.unarchiveProject.callsArgWith(1, "error") + @InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=> + err.should.equal "error" + @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal false + done() + + + it "should not call unarchiveProject if it is not inactive", (done)-> + delete @project.inactive + @DocstoreManager.unarchiveProject.callsArgWith(1) + @InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=> + @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal false + @ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal false + done() + + + describe "deactivateProject", -> + + beforeEach -> + + it "should call unarchiveProject and markAsInactive", (done)-> + @DocstoreManager.unarchiveProject.callsArgWith(1) + @ProjectUpdateHandler.markAsInactive.callsArgWith(1) + + @InactiveProjectManager.deactivateProject @project_id, (err)-> + @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsInactive.callsArgWith(@project_id).should.equal true + done() + + it "should not call markAsInactive if there was a problem unarchiving", (done)-> + @DocstoreManager.unarchiveProject.callsArgWith(1, "errorrr") + @ProjectUpdateHandler.markAsInactive.callsArgWith(1) + + @InactiveProjectManager.deactivateProject @project_id, (err)-> + err.should.equal "errorrr" + @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsInactive.callsArgWith(@project_id).should.equal false + done() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index c5c14c4359..1db3e50aad 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -42,6 +42,10 @@ describe "ProjectController", -> userCanAccessProject:sinon.stub() @EditorController = renameProject:sinon.stub() + @InactiveProjectManager = + reactivateProjectIfRequired:sinon.stub() + @ProjectUpdateHandler = + markOpened: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -57,6 +61,8 @@ describe "ProjectController", -> '../../models/Project': Project:@ProjectModel "../../models/User":User:@UserModel "../../managers/SecurityManager":@SecurityManager + "../InactiveData/InactiveProjectManager":@InactiveProjectManager + "./ProjectUpdateHandler":@ProjectUpdateHandler @user = _id:"!£123213kjljkl" @@ -282,6 +288,9 @@ describe "ProjectController", -> @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) @SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner" @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() + @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) + @ProjectUpdateHandler.markOpened.callsArgWith(1) + it "should render the project/editor page", (done)-> @res.render = (pageName, opts)=> @@ -321,3 +330,16 @@ describe "ProjectController", -> resCode.should.equal 401 done() @ProjectController.loadEditor @req, @res + + it "should reactivateProjectIfRequired", (done)-> + @res.render = (pageName, opts)=> + @InactiveProjectManager.reactivateProjectIfRequired.calledWith(@project_id).should.equal true + done() + @ProjectController.loadEditor @req, @res + + it "should mark project as opened", (done)-> + @res.render = (pageName, opts)=> + @ProjectUpdateHandler.markOpened.calledWith(@project_id).should.equal true + done() + @ProjectController.loadEditor @req, @res + diff --git a/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee index 3a275456e7..9aac418578 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee @@ -3,7 +3,7 @@ chai = require('chai').should() modulePath = "../../../../app/js/Features/Project/ProjectUpdateHandler.js" SandboxedModule = require('sandboxed-module') -describe 'updating a project', -> +describe 'ProjectUpdateHandler', -> beforeEach -> @@ -22,3 +22,36 @@ describe 'updating a project', -> now = Date.now()+"" date.substring(0,5).should.equal now.substring(0,5) done() + + describe "markAsOpened", -> + + it 'should send an update to mongo', (done)-> + project_id = "project_id" + @handler.markAsOpened project_id, (err)=> + args = @ProjectModel.update.args[0] + args[0]._id.should.equal project_id + date = args[1].lastOpened+"" + now = Date.now()+"" + date.substring(0,5).should.equal now.substring(0,5) + done() + + describe "markAsInactive", -> + + it 'should send an update to mongo', (done)-> + project_id = "project_id" + @handler.markAsInactive project_id, (err)=> + args = @ProjectModel.update.args[0] + args[0]._id.should.equal project_id + args[1].inactive.should.equal true + done() + + describe "markAsActive", -> + it 'should send an update to mongo', (done)-> + project_id = "project_id" + @handler.markAsActive project_id, (err)=> + args = @ProjectModel.update.args[0] + args[0]._id.should.equal project_id + args[1]["$unset"].inactive.should.equal true + done() + +