Skip to content

Commit

Permalink
Merge pull request #2617 from Northeastern-Electric-Racing/#2154-dele…
Browse files Browse the repository at this point in the history
…te-work-package-template

#2154 created endpoint and hook for delete wp template
  • Loading branch information
Peyton-McKee authored Jun 1, 2024
2 parents 20e8666 + 6c088b1 commit 7b8cde5
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 10 deletions.
14 changes: 14 additions & 0 deletions src/backend/src/controllers/work-packages.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,4 +241,18 @@ export default class WorkPackagesController {
next(error);
}
}

// Delete a work package template that corresponds to the given workPackageTemplateId
static async deleteWorkPackageTemplate(req: Request, res: Response, next: NextFunction) {
try {
const user = await getCurrentUser(res);
const { workPackageTemplateId } = req.params;
const organizationId = getOrganizationId(req.headers);

await WorkPackagesService.deleteWorkPackageTemplate(user, workPackageTemplateId, organizationId);
res.status(200).json({ message: `Successfully deleted work package template #${req.params.workPackageTemplateId}` });
} catch (error: unknown) {
next(error);
}
}
}
13 changes: 13 additions & 0 deletions src/backend/src/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import BillOfMaterialsService from '../services/boms.services';
import UsersService from '../services/users.services';
import { transformDate } from '../utils/datetime.utils';
import WorkPackagesService from '../services/work-packages.services';
import { writeFileSync } from 'fs';

const prisma = new PrismaClient();

Expand Down Expand Up @@ -257,6 +258,18 @@ const performSeed: () => Promise<void> = async () => {
const financeTeam: Team = await prisma.team.create(dbSeedAllTeams.financeTeam(monopolyMan.userId, organizationId));
const slackBotTeam: Team = await prisma.team.create(dbSeedAllTeams.meanGirls(regina.userId, organizationId));

/** Gets the current content of the .env file */
const currentEnv = require('dotenv').config().parsed;

currentEnv.DEV_ORGANIZATION_ID = organizationId;

/** Write the new .env file with the organization ID */
let stringifiedEnv = '';
Object.keys(currentEnv).forEach((key) => {
stringifiedEnv += `${key}=${currentEnv[key]}\n`;
});
writeFileSync('.env', stringifiedEnv);

/** Setting Team Members */
await TeamsService.setTeamMembers(
batman,
Expand Down
2 changes: 2 additions & 0 deletions src/backend/src/routes/work-package-templates.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ workPackageTemplatesRouter.post(
WorkPackagesController.createWorkPackageTemplate
);

workPackageTemplatesRouter.delete('/:workPackageTemplateId/delete', WorkPackagesController.deleteWorkPackageTemplate);

export default workPackageTemplatesRouter;
66 changes: 64 additions & 2 deletions src/backend/src/services/work-packages.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ import {
markDescriptionBulletsAsDeleted,
validateDescriptionBullets
} from '../utils/description-bullets.utils';
import { getBlockingWorkPackages, validateBlockedBys, validateBlockedByTemplates } from '../utils/work-packages.utils';
import {
deleteBlockingTemplates,
getBlockingWorkPackages,
validateBlockedBys,
validateBlockedByTemplates
} from '../utils/work-packages.utils';
import { workPackageTemplateTransformer } from '../transformers/work-package-template.transformer';
import { getWorkPackageTemplateQueryArgs } from '../prisma-query-args/work-package-template.query-args';
import { getDescriptionBulletQueryArgs } from '../prisma-query-args/description-bullets.query-args';
Expand Down Expand Up @@ -584,7 +589,6 @@ export default class WorkPackagesService {

const template = await prisma.work_Package_Template.findFirst({
where: {
dateDeleted: null,
workPackageTemplateId
},
...getWorkPackageTemplateQueryArgs(organizationId)
Expand Down Expand Up @@ -775,4 +779,62 @@ export default class WorkPackagesService {

return workPackageTemplateTransformer(updatedWorkPackageTemplate);
}

/**
* Deletes the Work Package template
* @param submitter The user who deleted the work package
* @param workPackageTemplateId The id of the work package template to be deleted
* @param organizationId The organization id that the user is in
*/
static async deleteWorkPackageTemplate(
submitter: User,
workPackageTemplateId: string,
organizationId: string
): Promise<void> {
// Verify submitter is allowed to delete work packages
if (!(await userHasPermission(submitter.userId, organizationId, isAdmin)))
throw new AccessDeniedAdminOnlyException('delete work package template');

const workPackageTemplate = await prisma.work_Package_Template.findUnique({
where: {
workPackageTemplateId
},
include: {
blocking: true
}
});

if (!workPackageTemplate) {
throw new NotFoundException('Work Package Template', workPackageTemplateId);
}

if (workPackageTemplate.dateDeleted) {
throw new DeletedException('Work Package Template', workPackageTemplateId);
}

if (workPackageTemplate.organizationId !== organizationId) {
throw new InvalidOrganizationException('Work Package Template');
}

const dateDeleted = new Date();

if (workPackageTemplate.blocking.length > 0) {
await deleteBlockingTemplates(workPackageTemplate, submitter);
}

// Soft delete the work package template by updating its related "deleted" fields
await prisma.work_Package_Template.update({
where: {
workPackageTemplateId
},
data: {
dateDeleted,
userDeleted: {
connect: {
userId: submitter.userId
}
}
}
});
}
}
60 changes: 59 additions & 1 deletion src/backend/src/utils/work-packages.utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Description_Bullet, Prisma, WBS_Element, Work_Package_Template } from '@prisma/client';
import { Description_Bullet, Prisma, User, WBS_Element, Work_Package_Template } from '@prisma/client';
import prisma from '../prisma/prisma';
import { HttpException, NotFoundException } from './errors.utils';
import { WbsNumber } from 'shared';
Expand Down Expand Up @@ -57,6 +57,64 @@ export const getBlockingWorkPackages = async (initialWorkPackage: Prisma.Work_Pa
return blockingWorkPackages;
};

interface WorkPackageTemplateWithBlocking extends Work_Package_Template {
blocking: Work_Package_Template[];
}

export const deleteBlockingTemplates = async (workPackageTemplate: WorkPackageTemplateWithBlocking, submitter: User) => {
const seenWorkPackageTemplateIds: Set<string> = new Set<string>(workPackageTemplate.workPackageTemplateId);

// blocking ids that still need to be updated
const blockingIdUpdateQueue: string[] = workPackageTemplate.blocking.map(
(blocking: Work_Package_Template) => blocking.workPackageTemplateId
);

while (blockingIdUpdateQueue.length > 0) {
const currentBlockingId = blockingIdUpdateQueue.pop();

if (!currentBlockingId) break;
if (seenWorkPackageTemplateIds.has(currentBlockingId)) continue; // if we've already seen it we skip it

seenWorkPackageTemplateIds.add(currentBlockingId);

// gets the current blocking work package template
const currentBlocking = await prisma.work_Package_Template.findUnique({
where: {
workPackageTemplateId: currentBlockingId
},
include: {
blocking: true
}
});

if (currentBlocking?.workPackageTemplateId === workPackageTemplate.workPackageTemplateId) {
throw new HttpException(400, 'Circular dependency detected');
}

if (!currentBlocking) throw new NotFoundException('Work Package Template', currentBlockingId);
if (currentBlocking.dateDeleted) continue; // skip if this work package template has been deleted
const newBlocking: string[] = currentBlocking.blocking.map((blocking) => blocking.workPackageTemplateId);
blockingIdUpdateQueue.push(...newBlocking);

const dateDeleted = new Date();

// delete the work package template
await prisma.work_Package_Template.update({
where: {
workPackageTemplateId: currentBlockingId
},
data: {
dateDeleted,
userDeleted: {
connect: {
userId: submitter.userId
}
}
}
});
}
};

export const validateBlockedBys = async (blockedBy: WbsNumber[], organizationId: string): Promise<WBS_Element[]> => {
blockedBy.forEach((dep: WbsNumber) => {
if (dep.workPackageNumber === 0) {
Expand Down
5 changes: 1 addition & 4 deletions src/backend/tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export const createTestUser = async (
export const resetUsers = async () => {
await prisma.project.deleteMany();
await prisma.work_Package.deleteMany();

await prisma.team_Type.deleteMany();
await prisma.material.deleteMany();
await prisma.manufacturer.deleteMany();
Expand Down Expand Up @@ -173,12 +172,10 @@ export const createTestOrganization = async () => {
});
};

export const createTestWorkPackageTemplate = async (organizationId?: string) => {
export const createTestWorkPackageTemplate = async (user: User, organizationId?: string) => {
if (!organizationId) organizationId = await createTestOrganization().then((org) => org.organizationId);
if (!organizationId) throw new Error('Failed to create organization');

const user = await createTestUser(batmanAppAdmin, organizationId);

const workPackageTemplate = await prisma.work_Package_Template.create({
data: {
workPackageName: 'Work Package 1',
Expand Down
104 changes: 102 additions & 2 deletions src/backend/tests/unmocked/work-package-template.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import WorkPackageService from '../../src/services/work-packages.services';
import { AccessDeniedGuestException, HttpException } from '../../src/utils/errors.utils';
import {
AccessDeniedGuestException,
AccessDeniedAdminOnlyException,
DeletedException,
HttpException
} from '../../src/utils/errors.utils';
import { createTestOrganization, createTestUser, createTestWorkPackageTemplate, resetUsers } from '../test-utils';
import { batmanAppAdmin, supermanAdmin, theVisitorGuest } from '../test-data/users.test-data';
import { workPackageTemplateTransformer } from '../../src/transformers/work-package-template.transformer';
import prisma from '../../src/prisma/prisma';

describe('Work Package Template Tests', () => {
let orgId: string;
Expand Down Expand Up @@ -31,7 +37,8 @@ describe('Work Package Template Tests', () => {
});

it('get single work package template succeeds', async () => {
const createdWorkPackageTemplate = await createTestWorkPackageTemplate(orgId);
const testBatman = await createTestUser(batmanAppAdmin, orgId);
const createdWorkPackageTemplate = await createTestWorkPackageTemplate(testBatman, orgId);

const recievedWorkPackageTemplate = await WorkPackageService.getSingleWorkPackageTemplate(
await createTestUser(supermanAdmin, orgId),
Expand All @@ -42,4 +49,97 @@ describe('Work Package Template Tests', () => {
expect(recievedWorkPackageTemplate).toStrictEqual(workPackageTemplateTransformer(createdWorkPackageTemplate));
});
});

describe('Delete single work package template', () => {
it('fails if user is a guest', async () => {
await expect(
async () =>
await WorkPackageService.deleteWorkPackageTemplate(await createTestUser(theVisitorGuest, orgId), 'id', orgId)
).rejects.toThrow(new AccessDeniedAdminOnlyException('delete work package template'));
});

it('fails is the work package template ID is not found', async () => {
await expect(
async () =>
await WorkPackageService.deleteWorkPackageTemplate(await createTestUser(supermanAdmin, orgId), 'id1', orgId)
).rejects.toThrow(new HttpException(400, `Work Package Template with id: id1 not found!`));
});

it('fails is the work package template has already been deleted', async () => {
const testSuperman = await createTestUser(supermanAdmin, orgId);
const testWorkPackageTemplate = await createTestWorkPackageTemplate(testSuperman, orgId);
await WorkPackageService.deleteWorkPackageTemplate(testSuperman, testWorkPackageTemplate.workPackageTemplateId, orgId);

await expect(
async () =>
await WorkPackageService.deleteWorkPackageTemplate(
testSuperman,
testWorkPackageTemplate.workPackageTemplateId,
orgId
)
).rejects.toThrow(new DeletedException('Work Package Template', testWorkPackageTemplate.workPackageTemplateId));
});

it('succeeds and deletes all blocking templates', async () => {
const testSuperman = await createTestUser(supermanAdmin, orgId);
const [testWorkPackageTemplate1, testWorkPackageTemplate2, testWorkPackageTemplate3] = await Promise.all([
createTestWorkPackageTemplate(testSuperman, orgId),
createTestWorkPackageTemplate(testSuperman, orgId),
createTestWorkPackageTemplate(testSuperman, orgId)
]);

await prisma.work_Package_Template.update({
where: {
workPackageTemplateId: testWorkPackageTemplate3.workPackageTemplateId
},
data: {
blockedBy: {
connect: {
workPackageTemplateId: testWorkPackageTemplate2.workPackageTemplateId
}
}
}
});

await prisma.work_Package_Template.update({
where: {
workPackageTemplateId: testWorkPackageTemplate2.workPackageTemplateId
},
data: {
blockedBy: {
connect: {
workPackageTemplateId: testWorkPackageTemplate1.workPackageTemplateId
}
}
}
});

await WorkPackageService.deleteWorkPackageTemplate(
testSuperman,
testWorkPackageTemplate1.workPackageTemplateId,
orgId
);

const updatedTestWorkPackageTemplate1 = await WorkPackageService.getSingleWorkPackageTemplate(
testSuperman,
testWorkPackageTemplate1.workPackageTemplateId,
orgId
);

const updatedTestWorkPackageTemplate2 = await WorkPackageService.getSingleWorkPackageTemplate(
testSuperman,
testWorkPackageTemplate2.workPackageTemplateId,
orgId
);
const updatedTestWorkPackageTemplate3 = await WorkPackageService.getSingleWorkPackageTemplate(
testSuperman,
testWorkPackageTemplate3.workPackageTemplateId,
orgId
);

expect(updatedTestWorkPackageTemplate1.dateDeleted).not.toBe(null);
expect(updatedTestWorkPackageTemplate2.dateDeleted).not.toBe(null);
expect(updatedTestWorkPackageTemplate3.dateDeleted).not.toBe(null);
});
});
});
9 changes: 9 additions & 0 deletions src/frontend/src/apis/work-packages.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ export const getAllWorkPackageTemplates = () => {
};

/**
* Delete a work package template.
*
* @param workPackageTemplateId The work package template id to be deleted.
*/
export const deleteWorkPackageTemplate = (workPackageTemplateId: string) => {
return axios.delete<{ message: string }>(apiUrls.workPackageTemplateDelete(workPackageTemplateId));
};

/*
* Gets a single work package template from the database
* @returns a single work package template
*/
Expand Down
Loading

0 comments on commit 7b8cde5

Please sign in to comment.