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

#2223: Design Review mark user confirmation endpoint #2225

Merged
merged 16 commits into from
Mar 19, 2024
14 changes: 14 additions & 0 deletions src/backend/src/controllers/design-reviews.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,18 @@ export default class DesignReviewsController {
next(error);
}
}

// Mark the current user as confirmed for the given design review
static async markUserConfirmed(req: Request, res: Response, next: NextFunction) {
try {
const { availability } = req.body;
martin0he marked this conversation as resolved.
Show resolved Hide resolved
const { designReviewId } = req.params;
const user = await getCurrentUser(res);

const updatedDesignReview = await DesignReviewsService.markUserConfirmed(designReviewId, availability, user);
return res.status(200).json(updatedDesignReview);
} catch (error: unknown) {
next(error);
}
}
}
8 changes: 8 additions & 0 deletions src/backend/src/routes/design-reviews.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,12 @@ designReviewsRouter.post(
DesignReviewsController.editDesignReviews
);

designReviewsRouter.post(
'/:designReviewId/confirm-schedule',
body('availability').isArray(),
intMinZero(body('availability.*')),
validateInputs,
DesignReviewsController.markUserConfirmed
);

export default designReviewsRouter;
71 changes: 70 additions & 1 deletion src/backend/src/services/design-reviews.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
AccessDeniedException
} from '../utils/errors.utils';
import { getUsers, getPrismaQueryUserIds } from '../utils/users.utils';
import { validateMeetingTimes } from '../utils/design-reviews.utils';
import { isUserOnDesignReview, validateMeetingTimes } from '../utils/design-reviews.utils';
import designReviewQueryArgs from '../prisma-query-args/design-reviews.query-args';
import { designReviewTransformer } from '../transformers/design-reviews.transformer';
import { sendSlackDesignReviewNotification } from '../utils/slack.utils';
Expand Down Expand Up @@ -316,4 +316,73 @@ export default class DesignReviewsService {
});
return designReviewTransformer(updateDesignReview);
}

/**
* Edits a design review by confirming a given user's availability and also updating their schedule settings with the given availability
* @param submitter the member that is being confirmed
* @param designReviewId the id of the design review
* @param availability the given member's availabilities
* @returns the modified design review with its updated confirmedMembers
*/
static async markUserConfirmed(designReviewId: string, availability: number[], submitter: User): Promise<DesignReview> {
const designReview = await prisma.design_Review.findUnique({
where: { designReviewId },
...designReviewQueryArgs
});

if (!designReview) throw new NotFoundException('Design Review', designReviewId);

if (designReview.dateDeleted) throw new DeletedException('Design Review', designReviewId);

if (!isUserOnDesignReview(submitter, designReviewTransformer(designReview)))
throw new HttpException(400, 'Current user is not in the list of this design reviews members');

// Update user schedule settings
const validAvailability = validateMeetingTimes(availability);

await prisma.schedule_Settings.upsert({
where: { userId: submitter.userId },
update: {
availability: validAvailability
},
create: {
userId: submitter.userId,
personalGmail: '',
personalZoomLink: '',
availability: validAvailability
}
});

// set submitter as confirmed if they're not already
if (!designReview.confirmedMembers.map((user) => user.userId).includes(submitter.userId)) {
const updatedDesignReview = await prisma.design_Review.update({
where: { designReviewId },
...designReviewQueryArgs,
data: {
confirmedMembers: {
connect: {
userId: submitter.userId
}
}
}
});

// If all requested attendees have confirmed their schedule, mark design review as confirmed
if (
designReview.confirmedMembers.length ===
designReview.requiredMembers.length + designReview.optionalMembers.length
) {
await prisma.design_Review.update({
where: { designReviewId },
...designReviewQueryArgs,
data: {
status: Design_Review_Status.CONFIRMED
}
});
}

return designReviewTransformer(updatedDesignReview);
}
return designReviewTransformer(designReview);
}
}
19 changes: 13 additions & 6 deletions src/backend/src/utils/design-reviews.utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { DesignReview, User } from 'shared';
import { HttpException } from './errors.utils';

/**
* Validate meeting times
* @param nums the meeting times
* @returns the meeting times
*/
export function validateMeetingTimes(nums: number[]): number[] {
for (let i = 1; i < nums.length; i++) {
export const validateMeetingTimes = (nums: number[]): number[] => {
for (let i = 0; i < nums.length; i++) {
if (nums[i] < 0 || nums[i] > 83) {
throw new HttpException(400, 'meeting time must be between 0-83');
throw new HttpException(400, 'Meeting times have to be in range 0-83');
}
if (nums[i] !== nums[i - 1] + 1) {
throw new HttpException(400, 'meeting times must be consecutive');
if (i > 0 && nums[i] !== nums[i - 1] + 1) {
throw new HttpException(400, 'Meeting times have to be consecutive');
}
}
return nums;
}
};

export const isUserOnDesignReview = (user: User, designReview: DesignReview): boolean => {
const requiredMembers = designReview.requiredMembers.map((user) => user.userId);
const optionalMembers = designReview.optionalMembers.map((user) => user.userId);
return requiredMembers.includes(user.userId) || optionalMembers.includes(user.userId);
};
66 changes: 63 additions & 3 deletions src/backend/tests/design-reviews.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import {
designReview1,
designReview3,
designReview5,
prismaDesignReview1,
prismaDesignReview2,
prismaDesignReview3,
prismaDesignReview5,
sharedDesignReview1,
teamType1
} from './test-data/design-reviews.test-data';
import { aquaman, batman, theVisitor, wonderwoman } from './test-data/users.test-data';
import {
aquaman,
batman,
batmanScheduleSettings,
batmanWithScheduleSettings,
superman,
theVisitor,
wonderwoman
} from './test-data/users.test-data';
import prisma from '../src/prisma/prisma';
import {
AccessDeniedAdminOnlyException,
Expand Down Expand Up @@ -249,7 +259,7 @@ describe('Design Reviews', () => {
[],
[1, 4, 2, 3]
)
).rejects.toThrow(new HttpException(400, 'meeting times must be consecutive'));
).rejects.toThrow(new HttpException(400, 'Meeting times have to be consecutive'));
});

test('Edit Design Review fails when meeting times are consecutive and *above* 83', async () => {
Expand All @@ -271,7 +281,7 @@ describe('Design Reviews', () => {
[],
[84, 85]
)
).rejects.toThrow(new HttpException(400, 'meeting time must be between 0-83'));
).rejects.toThrow(new HttpException(400, 'Meeting times have to be in range 0-83'));
});

test('Edit Design Review fails when no docTemplateLink, and status is scheduled or done', async () => {
Expand Down Expand Up @@ -501,4 +511,54 @@ describe('Design Reviews', () => {
).rejects.toThrow(new NotFoundException('WBS Element', 15));
});
});

describe('Mark user confirmed tests', () => {
test('mark user confirmed succeeds', async () => {
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview5);
vi.spyOn(prisma.schedule_Settings, 'upsert').mockResolvedValue(batmanScheduleSettings);
const result = await DesignReviewsService.markUserConfirmed(
prismaDesignReview5.designReviewId,
[1, 2],
batmanWithScheduleSettings
);

expect(prisma.design_Review.findUnique).toHaveBeenCalledTimes(1);
expect(result.confirmedMembers).toEqual(designReview5.confirmedMembers);
});

test('Design Review was not found', async () => {
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(null);
await expect(() =>
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [0, 1, 2], batman)
).rejects.toThrow(new NotFoundException('Design Review', prismaDesignReview5.designReviewId));
});

test('Design Review was deleted', async () => {
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue({ ...prismaDesignReview1, dateDeleted: new Date() });
await expect(() =>
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [0, 1, 2], batman)
).rejects.toThrow(new DeletedException('Design Review', prismaDesignReview5.designReviewId));
});

test('User was not in required/optional members of design review', async () => {
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview5);
await expect(() =>
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [0, 1, 2], superman)
).rejects.toThrow(new HttpException(400, 'Current user is not in the list of this design reviews members'));
});

test('Availabilities were invalid - out of bounds', async () => {
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview5);
await expect(() =>
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [0, 85], batman)
).rejects.toThrow(new HttpException(400, 'Meeting times have to be in range 0-83'));
});

test('Availabilities were invalid - non-consecutive', async () => {
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview5);
await expect(() =>
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [1, 3], batman)
).rejects.toThrow(new HttpException(400, 'Meeting times have to be consecutive'));
});
});
});
56 changes: 55 additions & 1 deletion src/backend/tests/test-data/design-reviews.test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import {
TeamType,
Design_Review as PrismaDesignReview
} from '@prisma/client';
import { batman, sharedBatman, wonderwoman } from './users.test-data';
import {
batman,
sharedBatman,
wonderwoman,
wonderwomanMarkedWithScheduleSettings,
wonderwomanWithScheduleSettings
} from './users.test-data';
import { prismaWbsElement1 } from './wbs-element.test-data';
import {
DesignReview,
Expand Down Expand Up @@ -119,6 +125,33 @@ export const prismaDesignReview3: Prisma.Design_ReviewGetPayload<typeof designRe
teamType: teamType1
};

export const prismaDesignReview5: Prisma.Design_ReviewGetPayload<typeof designReviewQueryArgs> = {
designReviewId: '1',
dateScheduled: new Date('2024-03-25'),
meetingTimes: [0, 1, 2, 3],
dateCreated: new Date('2024-03-10'),
userCreatedId: wonderwoman.userId,
userCreated: wonderwoman,
status: PrismaDesignReviewStatus.CONFIRMED,
teamTypeId: '1',
teamType: teamType1,
location: null,
isOnline: true,
isInPerson: false,
zoomLink: null,
dateDeleted: null,
userDeletedId: null,
docTemplateLink: null,
wbsElementId: 1,
requiredMembers: [batman],
optionalMembers: [wonderwomanWithScheduleSettings],
confirmedMembers: [batman],
deniedMembers: [],
attendees: [wonderwoman],
userDeleted: null,
wbsElement: prismaWbsElement1
};

export const designReview3: DesignReview = {
designReviewId: '2',
dateScheduled: new Date('2024-03-25'),
Expand Down Expand Up @@ -161,3 +194,24 @@ export const sharedDesignReview1: SharedDesignReview = {
wbsName: 'car',
wbsNum: { carNumber: 1, projectNumber: 2, workPackageNumber: 0 }
};

export const designReview5: DesignReview = {
designReviewId: '1',
dateScheduled: new Date('2024-03-25'),
meetingTimes: [0, 1, 2, 3],
dateCreated: new Date('2024-03-10'),
userCreated: wonderwoman,
status: DesignReviewStatus.CONFIRMED,
teamType: teamType1,
isOnline: true,
isInPerson: false,
requiredMembers: [],
optionalMembers: [wonderwomanMarkedWithScheduleSettings],
confirmedMembers: [sharedBatman],
deniedMembers: [],
attendees: [wonderwoman],
wbsName: 'car',
wbsNum: { carNumber: 1, projectNumber: 2, workPackageNumber: 0 },
zoomLink: undefined,
userDeleted: undefined
};
30 changes: 30 additions & 0 deletions src/backend/tests/test-data/users.test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,33 @@ export const batmanUserScheduleSettings: UserScheduleSettings = {
personalZoomLink: 'https://zoom.us/j/gotham',
availability: []
};

export const wonderwomanScheduleSettings: Schedule_Settings = {
drScheduleSettingsId: 'wwschedule',
personalGmail: '[email protected]',
personalZoomLink: 'https://zoom.us/jk/athens',
availability: [3],
userId: 72
};

export const wonderwomanMarkedScheduleSettings: Schedule_Settings = {
drScheduleSettingsId: 'wwschedule',
personalGmail: '[email protected]',
personalZoomLink: 'https://zoom.us/jk/athens',
availability: [1, 2],
userId: 72
};

export const wonderwomanWithScheduleSettings: PrismaUser & { scheduleSettings: Schedule_Settings } = {
...wonderwoman,
scheduleSettings: {
...wonderwomanScheduleSettings
}
};

export const wonderwomanMarkedWithScheduleSettings: PrismaUser & { scheduleSettings: Schedule_Settings } = {
...wonderwoman,
scheduleSettings: {
...wonderwomanMarkedScheduleSettings
}
};
Loading