diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 44b6bd9ec2..ff6a52e471 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -157,3 +157,73 @@ module.exports = CollaboratorsHandler = return callback(error) {owner, members} = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers) callback(null, members) + + transferProjects: (from_user_id, to_user_id, callback=(err, projects) ->) -> + MEMBER_KEYS = ['collaberator_refs', 'readOnly_refs'] + + # jobs = [] + # project_ids = [] + # jobs.push (cb) -> + # Project.find { owner_ref: from_user_id }, { _id: 1 }, (error, projects = []) -> + # return callback(error) if error? + # project_ids = project_ids.concat(projects.map (p) -> p._id) + # Project.update { owner_ref: from_user_id }, { $set: { owner_ref: to_user_id }}, { multi: true }, cb + + # for key in MEMBER_KEYS + # do (key) -> + # jobs.push (cb) -> + # query = {} + # query[] + # Project.find { owner_ref: from_user_id }, { _id: 1 }, (error, projects = []) -> + # return callback(error) if error? + # project_ids = project_ids.concat(projects.map (p) -> p._id) + # Project.update { owner_ref: from_user_id }, { $set: { owner_ref: to_user_id }}, { multi: true }, cb + + # Find all the projects this user is part of so we can flush them to TPDS + query = + $or: + [{ owner_ref: from_user_id }] + .concat( + MEMBER_KEYS.map (key) -> + q = {} + q[key] = from_user_id + return q + ) # [{ collaberator_refs: from_user_id }, ...] + Project.find query, { _id: 1 }, (error, projects = []) -> + return callback(error) if error? + + project_ids = projects.map (p) -> p._id + logger.log {project_ids, from_user_id, to_user_id}, "transferring projects" + + update_jobs = [] + update_jobs.push (cb) -> + Project.update { owner_ref: from_user_id }, { $set: { owner_ref: to_user_id }}, { multi: true }, cb + for key in MEMBER_KEYS + do (key) -> + update_jobs.push (cb) -> + query = {} + addNewUserUpdate = $addToSet: {} + removeOldUserUpdate = $pull: {} + query[key] = from_user_id + removeOldUserUpdate.$pull[key] = from_user_id + addNewUserUpdate.$addToSet[key] = to_user_id + # Mongo won't let us pull and addToSet in the same query, so do it in + # two. Note we need to add first, since the query is based on the old user. + Project.update query, addNewUserUpdate, { multi: true }, (error) -> + return cb(error) if error? + Project.update query, removeOldUserUpdate, { multi: true }, cb + + # Flush each project to TPDS to add files to new user's Dropbox + ProjectEntityHandler = require("../Project/ProjectEntityHandler") + flush_jobs = [] + for project_id in project_ids + do (project_id) -> + flush_jobs.push (cb) -> + ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, cb + + # Flush in background, no need to block on this + async.series flush_jobs, (error) -> + if error? + logger.err {err: error, project_ids, from_user_id, to_user_id}, "error flushing tranferred projects to TPDS" + + async.series update_jobs, callback diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 9ba099f60b..ee231f7c9f 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -344,3 +344,103 @@ describe "CollaboratorsHandler", -> it 'should not call ProjectEditorHandler.buildOwnerAndMembersViews', -> @ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 0 + + describe 'transferProjects', -> + beforeEach -> + @from_user_id = "from-user-id" + @to_user_id = "to-user-id" + @projects = [{ + _id: "project-id-1" + }, { + _id: "project-id-2" + }] + @Project.find = sinon.stub().yields(null, @projects) + @Project.update = sinon.stub().yields() + @ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().yields() + + describe "successfully", -> + beforeEach -> + @CollaboratorHandler.transferProjects @from_user_id, @to_user_id, @callback + + it "should look up the affected projects", -> + @Project.find + .calledWith({ + $or : [ + { owner_ref: @from_user_id } + { collaberator_refs: @from_user_id } + { readOnly_refs: @from_user_id } + ] + }) + .should.equal true + + it "should transfer owned projects", -> + @Project.update + .calledWith({ + owner_ref: @from_user_id + }, { + $set: { owner_ref: @to_user_id } + }, { + multi: true + }) + .should.equal true + + it "should transfer collaborator projects", -> + @Project.update + .calledWith({ + collaberator_refs: @from_user_id + }, { + $addToSet: { collaberator_refs: @to_user_id } + }, { + multi: true + }) + .should.equal true + @Project.update + .calledWith({ + collaberator_refs: @from_user_id + }, { + $pull: { collaberator_refs: @from_user_id } + }, { + multi: true + }) + .should.equal true + + it "should transfer read only collaborator projects", -> + @Project.update + .calledWith({ + readOnly_refs: @from_user_id + }, { + $addToSet: { readOnly_refs: @to_user_id } + }, { + multi: true + }) + .should.equal true + @Project.update + .calledWith({ + readOnly_refs: @from_user_id + }, { + $pull: { readOnly_refs: @from_user_id } + }, { + multi: true + }) + .should.equal true + + it "should flush each project to the TPDS", -> + for project in @projects + @ProjectEntityHandler.flushProjectToThirdPartyDataStore + .calledWith(project._id) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true + + describe "when flushing to TPDS fails", -> + beforeEach -> + @ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().yields(new Error('oops')) + @CollaboratorHandler.transferProjects @from_user_id, @to_user_id, @callback + + it "should log an error", -> + @logger.err.called.should.equal true + + it "should not return an error since it happens in the background", -> + @callback.called.should.equal true + @callback.calledWith(new Error('oops')).should.equal false \ No newline at end of file