From 6c8fb7900a6b2956ae457fce70dc090ff16d1d0f Mon Sep 17 00:00:00 2001 From: martintrajanovski Date: Tue, 12 Nov 2024 12:08:23 +0100 Subject: [PATCH] feat: add parent proposal to the proposal document --- src/proposals/dto/update-proposal.dto.ts | 9 +++++ src/proposals/proposals.controller.ts | 6 ++- src/proposals/schemas/proposal.schema.ts | 15 ++++++++ test/Proposal.js | 49 ++++++++++++++++++++++-- test/RawDataset.js | 20 +++++++++- test/TestData.js | 4 +- 6 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/proposals/dto/update-proposal.dto.ts b/src/proposals/dto/update-proposal.dto.ts index 1ce6ce0f8..541c96fbe 100644 --- a/src/proposals/dto/update-proposal.dto.ts +++ b/src/proposals/dto/update-proposal.dto.ts @@ -124,6 +124,15 @@ export class UpdateProposalDto extends OwnableDto { @IsOptional() @IsObject() readonly metadata?: Record; + + @ApiProperty({ + type: String, + required: false, + description: "Parent proposal id.", + }) + @IsOptional() + @IsString() + readonly parentProposalId?: string; } export class PartialUpdateProposalDto extends PartialType(UpdateProposalDto) {} diff --git a/src/proposals/proposals.controller.ts b/src/proposals/proposals.controller.ts index ff509c0fc..04eeb9315 100644 --- a/src/proposals/proposals.controller.ts +++ b/src/proposals/proposals.controller.ts @@ -17,6 +17,7 @@ import { BadRequestException, Logger, InternalServerErrorException, + NotFoundException, } from "@nestjs/common"; import { Request } from "express"; import { ProposalsService } from "./proposals.service"; @@ -578,7 +579,10 @@ export class ProposalsController { const proposal = await this.proposalsService.findOne({ proposalId, }); - if (!proposal) return { canAccess: false }; + + if (!proposal) { + throw new NotFoundException(`Proposal with ${proposalId} not found`); + } const canAccess = await this.permissionChecker( Action.ProposalsRead, diff --git a/src/proposals/schemas/proposal.schema.ts b/src/proposals/schemas/proposal.schema.ts index 8e26a1692..970931252 100644 --- a/src/proposals/schemas/proposal.schema.ts +++ b/src/proposals/schemas/proposal.schema.ts @@ -167,6 +167,21 @@ export class ProposalClass extends OwnableClass { }) @Prop({ type: Object, required: false, default: {} }) metadata?: Record; + + @ApiProperty({ + type: String, + required: false, + description: "Parent proposal id", + default: null, + nullable: true, + }) + @Prop({ + type: String, + required: false, + default: null, + ref: "Proposal", + }) + parentProposalId: string; } export const ProposalSchema = SchemaFactory.createForClass(ProposalClass); diff --git a/test/Proposal.js b/test/Proposal.js index cb0ae6661..442e133e3 100644 --- a/test/Proposal.js +++ b/test/Proposal.js @@ -8,14 +8,16 @@ let accessTokenProposalIngestor = null, accessTokenAdminIngestor = null, accessTokenArchiveManager = null, defaultProposalId = null, + minimalProposalId = null, proposalId = null, + proposalWithParentId = null, attachmentId = null; describe("1500: Proposal: Simple Proposal", () => { before(() => { db.collection("Proposal").deleteMany({}); }); - beforeEach(async() => { + beforeEach(async () => { accessTokenProposalIngestor = await utils.getToken(appUrl, { username: "proposalIngestor", password: TestData.Accounts["proposalIngestor"]["password"], @@ -89,7 +91,7 @@ describe("1500: Proposal: Simple Proposal", () => { res.body.should.have.property("ownerGroup").and.be.string; res.body.should.have.property("proposalId").and.be.string; defaultProposalId = res.body["proposalId"]; - proposalId = encodeURIComponent(res.body["proposalId"]); + minimalProposalId = encodeURIComponent(res.body["proposalId"]); }); }); @@ -216,7 +218,46 @@ describe("1500: Proposal: Simple Proposal", () => { }); }); - it("0120: should delete this proposal attachment", async () => { + it("0120: adds a new proposal with parent proposal", async () => { + const proposalWithParentProposal = { + ...TestData.ProposalCorrectComplete, + proposalId: "20170268", + parentProposalId: proposalId, + }; + + return request(appUrl) + .post("/api/v3/Proposals") + .send(proposalWithParentProposal) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenProposalIngestor}` }) + .expect(TestData.EntryCreatedStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("ownerGroup").and.be.string; + res.body.should.have.property("proposalId").and.be.string; + proposalWithParentId = res.body.proposalId; + res.body.should.have.property("parentProposalId").and.be.string; + res.body.parentProposalId.should.be.equal(proposalId); + }); + }); + + it("0120: updates a proposal with a new parent proposal", async () => { + return request(appUrl) + .patch("/api/v3/Proposals/" + proposalWithParentId) + .send({ parentProposalId: minimalProposalId }) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + .expect(TestData.SuccessfulPatchStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("ownerGroup").and.be.string; + res.body.should.have.property("proposalId").and.be.string; + res.body.should.have.property("parentProposalId").and.be.string; + res.body.parentProposalId.should.be.equal(minimalProposalId); + }); + }); + + it("0130: should delete this proposal attachment", async () => { return request(appUrl) .delete( "/api/v3/Proposals/" + proposalId + "/attachments/" + attachmentId, @@ -226,7 +267,7 @@ describe("1500: Proposal: Simple Proposal", () => { .expect(TestData.SuccessfulDeleteStatusCode); }); - it("0130: admin can remove all existing proposals", async () => { + it("0140: admin can remove all existing proposals", async () => { return await request(appUrl) .get("/api/v3/Proposals") .set("Accept", "application/json") diff --git a/test/RawDataset.js b/test/RawDataset.js index 60563c898..887a0578b 100644 --- a/test/RawDataset.js +++ b/test/RawDataset.js @@ -18,7 +18,7 @@ describe("1900: RawDataset: Raw Datasets", () => { db.collection("Dataset").deleteMany({}); db.collection("Proposals").deleteMany({}); }); - beforeEach(async() => { + beforeEach(async () => { accessProposalToken = await utils.getToken(appUrl, { username: "proposalIngestor", password: TestData.Accounts["proposalIngestor"]["password"], @@ -46,6 +46,8 @@ describe("1900: RawDataset: Raw Datasets", () => { .then((res) => { res.body.should.have.property("ownerGroup").and.be.string; res.body.should.have.property("proposalId").and.be.string; + // NOTE: Add real proposal in the testdata instead of fixed (non-existing) one + TestData.RawCorrect.proposalId = res.body["proposalId"]; proposalId = encodeURIComponent(res.body["proposalId"]); }); }); @@ -280,6 +282,22 @@ describe("1900: RawDataset: Raw Datasets", () => { ); }); + it("01250: adds a new proposal for pattching in the existing dataset", async () => { + return request(appUrl) + .post("/api/v3/Proposals") + .send(TestData.ProposalCorrectComplete) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessProposalToken}` }) + .expect(TestData.EntryCreatedStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("ownerGroup").and.be.string; + res.body.should.have.property("proposalId").and.be.string; + // NOTE: Add real proposal in the testdata instead of fixed (non-existing) one + TestData.PatchProposal1.proposalId = res.body["proposalId"]; + }); + }); + it("0125: should update proposal of the dataset", async () => { return request(appUrl) .patch("/api/v3/datasets/" + pid) diff --git a/test/TestData.js b/test/TestData.js index a10e2feeb..97d9dad42 100644 --- a/test/TestData.js +++ b/test/TestData.js @@ -231,7 +231,7 @@ const TestData = { isPublished: false, ownerGroup: "p13388", accessGroups: [], - proposalId: "10.540.16635/20110123", + proposalId: "", runNumber: "123456", instrumentId: "1f016ec4-7a73-11ef-ae3e-439013069377", sampleId: "20c32b4e-7a73-11ef-9aec-5b9688aa3791i", @@ -838,7 +838,7 @@ const TestData = { }, PatchProposal1: { - proposalId: "10.540.16635/20240124", + proposalId: "", }, PatchInstrument1: {