Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2154 created endpoint and hook for delete wp template #2617

Merged
merged 19 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
65 changes: 64 additions & 1 deletion 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 @@ -775,4 +780,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);
}

Aaryan1203 marked this conversation as resolved.
Show resolved Hide resolved
if (workPackageTemplate.organizationId !== organizationId) {
throw new InvalidOrganizationException('Work Package Template');
}

const dateDeleted = new Date();

if (workPackageTemplate.blocking.length > 0) {
deleteBlockingTemplates(workPackageTemplate, submitter);
Aaryan1203 marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
}
}
}
});
}
}
61 changes: 60 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,65 @@ export const getBlockingWorkPackages = async (initialWorkPackage: Prisma.Work_Pa
return blockingWorkPackages;
};

// TODO: I can't find the proper prisma type for this like you did above
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
4 changes: 1 addition & 3 deletions src/backend/tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,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
114 changes: 112 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,107 @@ 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 = await createTestWorkPackageTemplate(testSuperman, orgId);
const testWorkPackageTemplate2 = await createTestWorkPackageTemplate(testSuperman, orgId);
const testWorkPackageTemplate3 = await createTestWorkPackageTemplate(testSuperman, orgId);

await prisma.work_Package_Template.update({
Aaryan1203 marked this conversation as resolved.
Show resolved Hide resolved
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 prisma.work_Package_Template.findFirst({
Aaryan1203 marked this conversation as resolved.
Show resolved Hide resolved
where: {
workPackageTemplateId: testWorkPackageTemplate1.workPackageTemplateId
},
include: {
Aaryan1203 marked this conversation as resolved.
Show resolved Hide resolved
blocking: true
}
});

const updatedTestWorkPackageTemplate2 = await prisma.work_Package_Template.findFirst({
where: {
workPackageTemplateId: testWorkPackageTemplate2.workPackageTemplateId
},
include: {
blocking: true
}
});

const updatedTestWorkPackageTemplate3 = await prisma.work_Package_Template.findFirst({
where: {
workPackageTemplateId: testWorkPackageTemplate3.workPackageTemplateId
},
include: {
blocking: true,
}
});
console.log(updatedTestWorkPackageTemplate1);
console.log(updatedTestWorkPackageTemplate2);
console.log(updatedTestWorkPackageTemplate3);
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
20 changes: 20 additions & 0 deletions src/frontend/src/hooks/work-packages.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
WorkPackageTemplateApiInputs,
editWorkPackageTemplate,
getAllWorkPackageTemplates,
deleteWorkPackageTemplate,
getSingleWorkPackageTemplate,
createSingleWorkPackageTemplate
} from '../apis/work-packages.api';
Expand Down Expand Up @@ -161,6 +162,25 @@ export const useAllWorkPackageTemplates = () => {
};

/**
* Custom React Hook to delete a work package template.
*/
export const useDeleteWorkPackageTemplate = () => {
const queryClient = useQueryClient();
return useMutation<{ message: string }, Error, string>(
['work package template', 'delete'],
async (workPackageTemplateId: string) => {
const { data } = await deleteWorkPackageTemplate(workPackageTemplateId);
return data;
},
{
onSuccess: () => {
queryClient.invalidateQueries(['work package template']);
}
}
);
};

/*
* Custom React Hook to get a single workpackage template
*/
export const useSingleWorkPackageTemplate = (workPackageTemplateId: string) => {
Expand Down
Loading
Loading