Skip to content

Commit 558bb23

Browse files
authored
Merge branch 'develop' into #2131-drc-create-modal
2 parents 248bc1e + fac7a7b commit 558bb23

22 files changed

Lines changed: 691 additions & 331 deletions

src/backend/src/controllers/design-reviews.controllers.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ export default class DesignReviewsController {
7070
const {
7171
dateScheduled,
7272
teamType,
73-
requiredMembers,
74-
optionalMembers,
73+
requiredMembersIds,
74+
optionalMembersIds,
7575
isOnline,
7676
isInPerson,
7777
zoomLink,
@@ -92,8 +92,8 @@ export default class DesignReviewsController {
9292
designReviewId,
9393
dateScheduled,
9494
teamType.teamTypeId,
95-
requiredMembers,
96-
optionalMembers,
95+
requiredMembersIds,
96+
optionalMembersIds,
9797
isOnline,
9898
isInPerson,
9999
zoomLink,
@@ -108,4 +108,18 @@ export default class DesignReviewsController {
108108
next(error);
109109
}
110110
}
111+
112+
// Mark the current user as confirmed for the given design review
113+
static async markUserConfirmed(req: Request, res: Response, next: NextFunction) {
114+
try {
115+
const { availability } = req.body;
116+
const { designReviewId } = req.params;
117+
const user = await getCurrentUser(res);
118+
119+
const updatedDesignReview = await DesignReviewsService.markUserConfirmed(designReviewId, availability, user);
120+
return res.status(200).json(updatedDesignReview);
121+
} catch (error: unknown) {
122+
next(error);
123+
}
124+
}
111125
}

src/backend/src/routes/design-reviews.routes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,12 @@ designReviewsRouter.post(
5151
DesignReviewsController.editDesignReviews
5252
);
5353

54+
designReviewsRouter.post(
55+
'/:designReviewId/confirm-schedule',
56+
body('availability').isArray(),
57+
intMinZero(body('availability.*')),
58+
validateInputs,
59+
DesignReviewsController.markUserConfirmed
60+
);
61+
5462
export default designReviewsRouter;

src/backend/src/services/design-reviews.services.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
AccessDeniedException
1111
} from '../utils/errors.utils';
1212
import { getUsers, getPrismaQueryUserIds } from '../utils/users.utils';
13-
import { validateMeetingTimes } from '../utils/design-reviews.utils';
13+
import { isUserOnDesignReview, validateMeetingTimes } from '../utils/design-reviews.utils';
1414
import designReviewQueryArgs from '../prisma-query-args/design-reviews.query-args';
1515
import { designReviewTransformer } from '../transformers/design-reviews.transformer';
1616
import { sendSlackDesignReviewNotification } from '../utils/slack.utils';
@@ -300,4 +300,73 @@ export default class DesignReviewsService {
300300
});
301301
return designReviewTransformer(updateDesignReview);
302302
}
303+
304+
/**
305+
* Edits a design review by confirming a given user's availability and also updating their schedule settings with the given availability
306+
* @param submitter the member that is being confirmed
307+
* @param designReviewId the id of the design review
308+
* @param availability the given member's availabilities
309+
* @returns the modified design review with its updated confirmedMembers
310+
*/
311+
static async markUserConfirmed(designReviewId: string, availability: number[], submitter: User): Promise<DesignReview> {
312+
const designReview = await prisma.design_Review.findUnique({
313+
where: { designReviewId },
314+
...designReviewQueryArgs
315+
});
316+
317+
if (!designReview) throw new NotFoundException('Design Review', designReviewId);
318+
319+
if (designReview.dateDeleted) throw new DeletedException('Design Review', designReviewId);
320+
321+
if (!isUserOnDesignReview(submitter, designReviewTransformer(designReview)))
322+
throw new HttpException(400, 'Current user is not in the list of this design reviews members');
323+
324+
// Update user schedule settings
325+
const validAvailability = validateMeetingTimes(availability);
326+
327+
await prisma.schedule_Settings.upsert({
328+
where: { userId: submitter.userId },
329+
update: {
330+
availability: validAvailability
331+
},
332+
create: {
333+
userId: submitter.userId,
334+
personalGmail: '',
335+
personalZoomLink: '',
336+
availability: validAvailability
337+
}
338+
});
339+
340+
// set submitter as confirmed if they're not already
341+
if (!designReview.confirmedMembers.map((user) => user.userId).includes(submitter.userId)) {
342+
const updatedDesignReview = await prisma.design_Review.update({
343+
where: { designReviewId },
344+
...designReviewQueryArgs,
345+
data: {
346+
confirmedMembers: {
347+
connect: {
348+
userId: submitter.userId
349+
}
350+
}
351+
}
352+
});
353+
354+
// If all requested attendees have confirmed their schedule, mark design review as confirmed
355+
if (
356+
designReview.confirmedMembers.length ===
357+
designReview.requiredMembers.length + designReview.optionalMembers.length
358+
) {
359+
await prisma.design_Review.update({
360+
where: { designReviewId },
361+
...designReviewQueryArgs,
362+
data: {
363+
status: Design_Review_Status.CONFIRMED
364+
}
365+
});
366+
}
367+
368+
return designReviewTransformer(updatedDesignReview);
369+
}
370+
return designReviewTransformer(designReview);
371+
}
303372
}
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1+
import { DesignReview, User } from 'shared';
12
import { HttpException } from './errors.utils';
23

34
/**
45
* Validate meeting times
56
* @param nums the meeting times
67
* @returns the meeting times
78
*/
8-
export function validateMeetingTimes(nums: number[]): number[] {
9-
for (let i = 1; i < nums.length; i++) {
9+
export const validateMeetingTimes = (nums: number[]): number[] => {
10+
for (let i = 0; i < nums.length; i++) {
1011
if (nums[i] < 0 || nums[i] > 83) {
11-
throw new HttpException(400, 'meeting time must be between 0-83');
12+
throw new HttpException(400, 'Meeting times have to be in range 0-83');
1213
}
13-
if (nums[i] !== nums[i - 1] + 1) {
14-
throw new HttpException(400, 'meeting times must be consecutive');
14+
if (i > 0 && nums[i] !== nums[i - 1] + 1) {
15+
throw new HttpException(400, 'Meeting times have to be consecutive');
1516
}
1617
}
1718
return nums;
18-
}
19+
};
20+
21+
export const isUserOnDesignReview = (user: User, designReview: DesignReview): boolean => {
22+
const requiredMembers = designReview.requiredMembers.map((user) => user.userId);
23+
const optionalMembers = designReview.optionalMembers.map((user) => user.userId);
24+
return requiredMembers.includes(user.userId) || optionalMembers.includes(user.userId);
25+
};

src/backend/tests/design-reviews.test.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import {
22
designReview1,
33
designReview3,
4+
designReview5,
45
prismaDesignReview1,
56
prismaDesignReview2,
67
prismaDesignReview3,
8+
prismaDesignReview5,
79
sharedDesignReview1,
810
teamType1
911
} from './test-data/design-reviews.test-data';
10-
import { aquaman, batman, theVisitor, wonderwoman } from './test-data/users.test-data';
12+
import {
13+
aquaman,
14+
batman,
15+
batmanScheduleSettings,
16+
batmanWithScheduleSettings,
17+
superman,
18+
theVisitor,
19+
wonderwoman
20+
} from './test-data/users.test-data';
1121
import prisma from '../src/prisma/prisma';
1222
import {
1323
AccessDeniedAdminOnlyException,
@@ -249,7 +259,7 @@ describe('Design Reviews', () => {
249259
[],
250260
[1, 4, 2, 3]
251261
)
252-
).rejects.toThrow(new HttpException(400, 'meeting times must be consecutive'));
262+
).rejects.toThrow(new HttpException(400, 'Meeting times have to be consecutive'));
253263
});
254264

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

277287
test('Edit Design Review fails when no docTemplateLink, and status is scheduled or done', async () => {
@@ -485,4 +495,54 @@ describe('Design Reviews', () => {
485495
).rejects.toThrow(new NotFoundException('WBS Element', 15));
486496
});
487497
});
498+
499+
describe('Mark user confirmed tests', () => {
500+
test('mark user confirmed succeeds', async () => {
501+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview5);
502+
vi.spyOn(prisma.schedule_Settings, 'upsert').mockResolvedValue(batmanScheduleSettings);
503+
const result = await DesignReviewsService.markUserConfirmed(
504+
prismaDesignReview5.designReviewId,
505+
[1, 2],
506+
batmanWithScheduleSettings
507+
);
508+
509+
expect(prisma.design_Review.findUnique).toHaveBeenCalledTimes(1);
510+
expect(result.confirmedMembers).toEqual(designReview5.confirmedMembers);
511+
});
512+
513+
test('Design Review was not found', async () => {
514+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(null);
515+
await expect(() =>
516+
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [0, 1, 2], batman)
517+
).rejects.toThrow(new NotFoundException('Design Review', prismaDesignReview5.designReviewId));
518+
});
519+
520+
test('Design Review was deleted', async () => {
521+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue({ ...prismaDesignReview1, dateDeleted: new Date() });
522+
await expect(() =>
523+
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [0, 1, 2], batman)
524+
).rejects.toThrow(new DeletedException('Design Review', prismaDesignReview5.designReviewId));
525+
});
526+
527+
test('User was not in required/optional members of design review', async () => {
528+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview5);
529+
await expect(() =>
530+
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [0, 1, 2], superman)
531+
).rejects.toThrow(new HttpException(400, 'Current user is not in the list of this design reviews members'));
532+
});
533+
534+
test('Availabilities were invalid - out of bounds', async () => {
535+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview5);
536+
await expect(() =>
537+
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [0, 85], batman)
538+
).rejects.toThrow(new HttpException(400, 'Meeting times have to be in range 0-83'));
539+
});
540+
541+
test('Availabilities were invalid - non-consecutive', async () => {
542+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview5);
543+
await expect(() =>
544+
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [1, 3], batman)
545+
).rejects.toThrow(new HttpException(400, 'Meeting times have to be consecutive'));
546+
});
547+
});
488548
});

src/backend/tests/test-data/design-reviews.test-data.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import {
44
TeamType,
55
Design_Review as PrismaDesignReview
66
} from '@prisma/client';
7-
import { batman, sharedBatman, wonderwoman } from './users.test-data';
7+
import {
8+
batman,
9+
sharedBatman,
10+
wonderwoman,
11+
wonderwomanMarkedWithScheduleSettings,
12+
wonderwomanWithScheduleSettings
13+
} from './users.test-data';
814
import { prismaWbsElement1 } from './wbs-element.test-data';
915
import {
1016
DesignReview,
@@ -119,6 +125,33 @@ export const prismaDesignReview3: Prisma.Design_ReviewGetPayload<typeof designRe
119125
teamType: teamType1
120126
};
121127

128+
export const prismaDesignReview5: Prisma.Design_ReviewGetPayload<typeof designReviewQueryArgs> = {
129+
designReviewId: '1',
130+
dateScheduled: new Date('2024-03-25'),
131+
meetingTimes: [0, 1, 2, 3],
132+
dateCreated: new Date('2024-03-10'),
133+
userCreatedId: wonderwoman.userId,
134+
userCreated: wonderwoman,
135+
status: PrismaDesignReviewStatus.CONFIRMED,
136+
teamTypeId: '1',
137+
teamType: teamType1,
138+
location: null,
139+
isOnline: true,
140+
isInPerson: false,
141+
zoomLink: null,
142+
dateDeleted: null,
143+
userDeletedId: null,
144+
docTemplateLink: null,
145+
wbsElementId: 1,
146+
requiredMembers: [batman],
147+
optionalMembers: [wonderwomanWithScheduleSettings],
148+
confirmedMembers: [batman],
149+
deniedMembers: [],
150+
attendees: [wonderwoman],
151+
userDeleted: null,
152+
wbsElement: prismaWbsElement1
153+
};
154+
122155
export const designReview3: DesignReview = {
123156
designReviewId: '2',
124157
dateScheduled: new Date('2024-03-25'),
@@ -161,3 +194,24 @@ export const sharedDesignReview1: SharedDesignReview = {
161194
wbsName: 'car',
162195
wbsNum: { carNumber: 1, projectNumber: 2, workPackageNumber: 0 }
163196
};
197+
198+
export const designReview5: DesignReview = {
199+
designReviewId: '1',
200+
dateScheduled: new Date('2024-03-25'),
201+
meetingTimes: [0, 1, 2, 3],
202+
dateCreated: new Date('2024-03-10'),
203+
userCreated: wonderwoman,
204+
status: DesignReviewStatus.CONFIRMED,
205+
teamType: teamType1,
206+
isOnline: true,
207+
isInPerson: false,
208+
requiredMembers: [],
209+
optionalMembers: [wonderwomanMarkedWithScheduleSettings],
210+
confirmedMembers: [sharedBatman],
211+
deniedMembers: [],
212+
attendees: [wonderwoman],
213+
wbsName: 'car',
214+
wbsNum: { carNumber: 1, projectNumber: 2, workPackageNumber: 0 },
215+
zoomLink: undefined,
216+
userDeleted: undefined
217+
};

src/backend/tests/test-data/users.test-data.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,33 @@ export const batmanUserScheduleSettings: UserScheduleSettings = {
162162
personalZoomLink: 'https://zoom.us/j/gotham',
163163
availability: []
164164
};
165+
166+
export const wonderwomanScheduleSettings: Schedule_Settings = {
167+
drScheduleSettingsId: 'wwschedule',
168+
personalGmail: 'diana@gmail.com',
169+
personalZoomLink: 'https://zoom.us/jk/athens',
170+
availability: [3],
171+
userId: 72
172+
};
173+
174+
export const wonderwomanMarkedScheduleSettings: Schedule_Settings = {
175+
drScheduleSettingsId: 'wwschedule',
176+
personalGmail: 'diana@gmail.com',
177+
personalZoomLink: 'https://zoom.us/jk/athens',
178+
availability: [1, 2],
179+
userId: 72
180+
};
181+
182+
export const wonderwomanWithScheduleSettings: PrismaUser & { scheduleSettings: Schedule_Settings } = {
183+
...wonderwoman,
184+
scheduleSettings: {
185+
...wonderwomanScheduleSettings
186+
}
187+
};
188+
189+
export const wonderwomanMarkedWithScheduleSettings: PrismaUser & { scheduleSettings: Schedule_Settings } = {
190+
...wonderwoman,
191+
scheduleSettings: {
192+
...wonderwomanMarkedScheduleSettings
193+
}
194+
};

0 commit comments

Comments
 (0)