Skip to content

Commit

Permalink
Merge pull request #3069 from Northeastern-Electric-Racing/#2942onboa…
Browse files Browse the repository at this point in the history
…rding-create-toggle-checklist-endpoint

#2942onboarding create toggle checklist endpoint
  • Loading branch information
Peyton-McKee authored Dec 22, 2024
2 parents 3c4e573 + f778a64 commit 8734178
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 4 deletions.
11 changes: 11 additions & 0 deletions src/backend/src/controllers/onboarding.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ export default class OnboardingController {
}
}

static async toggleChecklist(req: Request, res: Response, next: NextFunction) {
try {
const { checklistId } = req.params;

const updatedItem = await OnboardingServices.toggleChecklist(checklistId, req.currentUser, req.organization);
res.status(200).json(updatedItem);
} catch (error: unknown) {
return next(error);
}
}

static async downloadImage(req: Request, res: Response, next: NextFunction) {
try {
const { fileId } = req.params;
Expand Down
2 changes: 2 additions & 0 deletions src/backend/src/routes/onboarding.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ onboardingRouter.post(

onboardingRouter.post('/checklist/delete/:checklistId', OnboardingController.deleteChecklist);

onboardingRouter.post('/checklists/item/:checklistId/checked', OnboardingController.toggleChecklist);

onboardingRouter.get('/image/:fileId', OnboardingController.downloadImage);

export default onboardingRouter;
100 changes: 97 additions & 3 deletions src/backend/src/services/onboarding.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class OnboardingServices {
static async getAllChecklists(organization: Organization) {
const allChecklists = await prisma.checklist.findMany({
where: { organizationId: organization.organizationId, dateDeleted: null, parentChecklistId: null },
include: { subtasks: true, teamType: true }
include: { subtasks: true, teamType: true, usersChecked: true }
});

return allChecklists;
Expand All @@ -36,7 +36,7 @@ export default class OnboardingServices {
dateDeleted: null,
parentChecklistId: null
},
include: { subtasks: true, teamType: true }
include: { subtasks: true, teamType: true, usersChecked: true }
});
return generalChecklists;
}
Expand Down Expand Up @@ -309,7 +309,6 @@ export default class OnboardingServices {
throw new DeletedException('Checklist', checklistId);
}

// delete all subtasks
await prisma.checklist.updateMany({
where: { parentChecklistId: checklistId },
data: { dateDeleted: new Date(), userDeletedId: deleter.userId }
Expand All @@ -321,6 +320,101 @@ export default class OnboardingServices {
});
}

/**
* Toggles a user's check on a checklist
* @param checklistId the id of the checklist to toggle
* @param userId the id of the user to toggle
* @returns the updated checklist
*/
static async toggleChecklist(checklistId: string, user: User, organization: Organization) {
const checklist = await prisma.checklist.findUnique({
where: { checklistId, organizationId: organization.organizationId },
include: { usersChecked: true, subtasks: { where: { dateDeleted: null }, include: { usersChecked: true } } }
});

if (!checklist) {
throw new NotFoundException('Checklist', checklistId);
}

if (checklist.dateDeleted) {
throw new DeletedException('Checklist', checklistId);
}

const { userId } = user;
const isChecked = checklist.usersChecked.some((user) => user.userId === userId);

if (
checklist.subtasks.length > 0 &&
!checklist.subtasks.every((subtask) => subtask.usersChecked.some((user) => user.userId === userId))
) {
throw new HttpException(400, 'Cannot check off this checklist item because not all of its subtasks are checked.');
}

if (isChecked) {
await prisma.checklist.update({
where: { checklistId },
data: {
usersChecked: {
disconnect: { userId }
}
}
});
} else {
await prisma.checklist.update({
where: { checklistId },
data: {
usersChecked: {
connect: { userId }
}
}
});
}

// Check off the parent checklist if all subtasks are checked
if (checklist.parentChecklistId) {
const parentChecklist = await prisma.checklist.findUnique({
where: { checklistId: checklist.parentChecklistId },
include: {
subtasks: {
where: { dateDeleted: null },
include: { usersChecked: true }
}
}
});

if (parentChecklist) {
const allSubtasksChecked = parentChecklist.subtasks.every((subtask) =>
subtask.usersChecked.some((user) => user.userId === userId)
);
if (allSubtasksChecked) {
await prisma.checklist.update({
where: { checklistId: parentChecklist.checklistId },
data: {
usersChecked: {
connect: { userId }
}
}
});
} else {
await prisma.checklist.update({
where: { checklistId: parentChecklist.checklistId },
data: {
usersChecked: {
disconnect: { userId }
}
}
});
}
}
}
const updatedChecklist = await prisma.checklist.findUnique({
where: { checklistId },
include: { usersChecked: true }
});

return updatedChecklist;
}

static async downloadImage(fileId: string) {
const fileData = await downloadImageFile(fileId);

Expand Down
3 changes: 2 additions & 1 deletion src/backend/tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ export const createTestChecklist = async (
},
include: {
subtasks: true,
teamType: true
teamType: true,
usersChecked: true
}
});

Expand Down
151 changes: 151 additions & 0 deletions src/backend/tests/unmocked/onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,4 +502,155 @@ describe('Onboarding tests', () => {
expect(newChildChecklist!.dateDeleted).not.toBeNull();
});
});

describe('Toggle Checklist Item', () => {
it('Toggles checklist item and updates the parent checklist item if all children checked', async () => {
const batman = await createTestUser(batmanAppAdmin, orgId);

const parentChecklist = await createTestChecklist(batman, orgId, 'Parent Checklist', undefined, undefined, undefined);
const parentChecklistItem = await createTestChecklist(
batman,
orgId,
'Parent Checklist Item',
undefined,
undefined,
parentChecklist.checklistId
);

const childChecklistItem1 = await createTestChecklist(
batman,
orgId,
'Child Checklist Item 1',
undefined,
undefined,
parentChecklistItem.checklistId
);

const childChecklistItem2 = await createTestChecklist(
batman,
orgId,
'Child Checklist Item 2',
undefined,
undefined,
parentChecklistItem.checklistId
);

expect(childChecklistItem1?.usersChecked.length).toBe(0);
expect(childChecklistItem2?.usersChecked.length).toBe(0);

await OnboardingServices.toggleChecklist(childChecklistItem1.checklistId, batman, organization);

const updatedChildItem1 = await prisma.checklist.findUnique({
where: { checklistId: childChecklistItem1.checklistId },
include: { usersChecked: true }
});

const partiallyUpdatedParentItem = await prisma.checklist.findUnique({
where: { checklistId: parentChecklistItem.checklistId },
include: { usersChecked: true }
});

expect(updatedChildItem1?.usersChecked.length).toBe(1);
expect(partiallyUpdatedParentItem?.usersChecked.length).toBe(0);

// check all child items to update parent item
await OnboardingServices.toggleChecklist(childChecklistItem2.checklistId, batman, organization);

const updatedChildItem2 = await prisma.checklist.findUnique({
where: { checklistId: childChecklistItem2.checklistId },
include: { usersChecked: true }
});
expect(updatedChildItem2?.usersChecked.length).toBe(1);

const fullyUpdatedParentItem = await prisma.checklist.findUnique({
where: { checklistId: parentChecklistItem.checklistId },
include: { usersChecked: true }
});
expect(fullyUpdatedParentItem?.usersChecked.length).toBe(1);

// uncheck child item to automatically uncheck parent item
await OnboardingServices.toggleChecklist(childChecklistItem1.checklistId, batman, organization);

const revertedChildItem1 = await prisma.checklist.findUnique({
where: { checklistId: childChecklistItem1.checklistId },
include: { usersChecked: true }
});
expect(revertedChildItem1?.usersChecked.length).toBe(0);

const revertedParentItem = await prisma.checklist.findUnique({
where: { checklistId: parentChecklistItem.checklistId },
include: { usersChecked: true }
});
expect(revertedParentItem?.usersChecked.length).toBe(0);
});

it('throws NotFoundException when toggling a non-existing checklist item', async () => {
const batman = await createTestUser(batmanAppAdmin, orgId);
await expect(OnboardingServices.toggleChecklist('nonExistingId', batman, organization)).rejects.toThrow(
new NotFoundException('Checklist', 'nonExistingId')
);
});

it('throws DeletedException when toggling a deleted checklist item', async () => {
const batman = await createTestUser(batmanAppAdmin, orgId);
const checklist = await createTestChecklist(batman, orgId, 'Checklist to Delete');
await prisma.checklist.update({
where: { checklistId: checklist.checklistId },
data: { dateDeleted: new Date() }
});

await expect(OnboardingServices.toggleChecklist(checklist.checklistId, batman, organization)).rejects.toThrow(
new DeletedException('Checklist', checklist.checklistId)
);
});

it('throws HttpException when trying to toggle a parent checklist before all children are checked', async () => {
const batman = await createTestUser(batmanAppAdmin, orgId);

const parentChecklist = await createTestChecklist(batman, orgId, 'Parent Checklist');
const parentChecklistItem = await createTestChecklist(
batman,
orgId,
'Parent Checklist Item',
undefined,
undefined,
parentChecklist.checklistId
);

const childChecklistItem1 = await createTestChecklist(
batman,
orgId,
'Child Checklist Item 1',
undefined,
undefined,
parentChecklistItem.checklistId
);

await createTestChecklist(
batman,
orgId,
'Child Checklist Item 2',
undefined,
undefined,
parentChecklistItem.checklistId
);

await OnboardingServices.toggleChecklist(childChecklistItem1.checklistId, batman, organization);

await expect(
async () => await OnboardingServices.toggleChecklist(parentChecklist.checklistId, batman, organization)
).rejects.toThrowError('Cannot check off this checklist item because not all of its subtasks are checked.');
});

it('Succeeds and toggles a checklist without any subtasks', async () => {
const batman = await createTestUser(batmanAppAdmin, orgId);
const checklist = await createTestChecklist(batman, orgId, 'Checklist 1');
await OnboardingServices.toggleChecklist(checklist.checklistId, batman, organization);
const updatedChecklist = await prisma.checklist.findUnique({
where: { checklistId: checklist.checklistId },
include: { usersChecked: true }
});
expect(updatedChecklist?.usersChecked.length).toBe(1);
});
});
});

0 comments on commit 8734178

Please sign in to comment.