Skip to content

Commit a33100d

Browse files
committed
Merge branch 'develop' into #2131-drc-create-modal
2 parents 1deba36 + 28970fe commit a33100d

38 files changed

Lines changed: 1713 additions & 141 deletions

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,43 @@ export default class DesignReviewController {
2424
}
2525
}
2626

27+
static async createDesignReview(req: Request, res: Response, next: NextFunction) {
28+
try {
29+
const submitter: User = await getCurrentUser(res);
30+
const {
31+
dateScheduled,
32+
teamTypeId,
33+
requiredMemberIds,
34+
optionalMemberIds,
35+
location,
36+
isOnline,
37+
isInPerson,
38+
zoomLink,
39+
docTemplateLink,
40+
wbsNum,
41+
meetingTimes
42+
} = req.body;
43+
44+
const createdDesignReview = await DesignReviewService.createDesignReview(
45+
submitter,
46+
dateScheduled,
47+
teamTypeId,
48+
requiredMemberIds,
49+
optionalMemberIds,
50+
isOnline,
51+
isInPerson,
52+
docTemplateLink,
53+
wbsNum,
54+
meetingTimes,
55+
zoomLink,
56+
location
57+
);
58+
return res.status(200).json(createdDesignReview);
59+
} catch (error: unknown) {
60+
next(error);
61+
}
62+
}
63+
2764
static async getSingleDesignReview(req: Request, res: Response, next: NextFunction) {
2865
try {
2966
const drId: string = req.params.designReviewId;

src/backend/src/prisma/schema.prisma

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ model TeamType {
597597
model Design_Review {
598598
designReviewId String @id @default(uuid())
599599
dateScheduled DateTime @db.Date
600-
// Meeting times are an integer between 0 and 48 representing time after 9am in 15 minute increments, eg. 0 = 9:00am, 5 = 10:15 am
600+
// Meeting times are an integer between 0 and 83 representing time after 9am in 15 minute increments, eg. 0 = 9:00am, 5 = 10:15 am
601601
meetingTimes Int[]
602602
dateCreated DateTime @default(now()) @db.Timestamp(3)
603603
userCreated User @relation(fields: [userCreatedId], references: [userId], name: "designReviewCreator")

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import express from 'express';
22
import DesignReviewController from '../controllers/design-review.controllers';
3+
import { validateInputs } from '../utils/utils';
4+
import { body } from 'express-validator';
5+
import { intMinZero, nonEmptyString, isDate } from '../utils/validation.utils';
36

47
const designReviewRouter = express.Router();
58

@@ -8,4 +11,24 @@ designReviewRouter.get('/', DesignReviewController.getAllDesignReviews);
811
designReviewRouter.delete('/:designReviewId/delete', DesignReviewController.deleteDesignReview);
912
designReviewRouter.get('/:designReviewId', DesignReviewController.getSingleDesignReview);
1013

14+
designReviewRouter.post(
15+
'/create',
16+
isDate(body('dateScheduled')),
17+
nonEmptyString(body('teamTypeId')),
18+
body('requiredMemberIds').isArray(),
19+
intMinZero(body('requiredMemberIds.*')),
20+
body('optionalMemberIds').isArray(),
21+
intMinZero(body('optionalMemberIds.*')),
22+
nonEmptyString(body('location').optional()),
23+
body('isOnline').isBoolean(),
24+
body('isInPerson').isBoolean(),
25+
nonEmptyString(body('zoomLink').optional()),
26+
nonEmptyString(body('docTemplateLink')).optional(),
27+
body('wbsNum'),
28+
body('meetingTimes').isArray(),
29+
intMinZero(body('meetingTimes.*')),
30+
validateInputs,
31+
DesignReviewController.createDesignReview
32+
);
33+
1134
export default designReviewRouter;

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

Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import { DesignReview, isAdmin } from 'shared';
1+
import { DesignReview, WbsNumber, isAdmin, isLeadership } from 'shared';
22
import prisma from '../prisma/prisma';
3-
import { AccessDeniedAdminOnlyException, DeletedException, NotFoundException } from '../utils/errors.utils';
4-
import { User } from '@prisma/client';
3+
import {
4+
AccessDeniedAdminOnlyException,
5+
DeletedException,
6+
NotFoundException,
7+
HttpException,
8+
AccessDeniedException
9+
} from '../utils/errors.utils';
10+
import { User, Design_Review_Status } from '@prisma/client';
511
import designReviewQueryArgs from '../prisma-query-args/design-review.query-args';
612
import { designReviewTransformer } from '../transformers/design-review.transformer';
13+
import { sendSlackDesignReviewNotification } from '../utils/slack.utils';
714

815
export default class DesignReviewService {
916
/**
@@ -45,6 +52,142 @@ export default class DesignReviewService {
4552
return designReviewTransformer(deletedDesignReview);
4653
}
4754

55+
/**
56+
* Creates a design review
57+
* @param submitter user who submitted the design review
58+
* @param dateScheduled when the design review is scheduled for
59+
* @param teamTypeId team type id of the design review
60+
* @param requiredMemberIds ids of the required members to attend the design review
61+
* @param optionalMemberIds ids of the optional members to attend the design reivew
62+
* @param isOnline if design review is online
63+
* @param isInPerson if design review is in person
64+
* @param docTemplateLink link to the doc template
65+
* @param wbsNum wbs number for the design review
66+
* @param meetingTimes the meeting times for the design review
67+
* @param zoomLink link for the zoom if design review is online
68+
* @param location location of the design review if in person
69+
* @returns a design review
70+
*/
71+
static async createDesignReview(
72+
submitter: User,
73+
dateScheduled: Date,
74+
teamTypeId: string,
75+
requiredMemberIds: number[],
76+
optionalMemberIds: number[],
77+
isOnline: boolean,
78+
isInPerson: boolean,
79+
docTemplateLink: string,
80+
wbsNum: WbsNumber,
81+
meetingTimes: number[],
82+
zoomLink?: string,
83+
location?: string
84+
): Promise<DesignReview> {
85+
if (!isLeadership(submitter.role)) throw new AccessDeniedException('create design review');
86+
87+
const teamType = await prisma.teamType.findFirst({
88+
where: { teamTypeId }
89+
});
90+
91+
if (!teamType) {
92+
throw new NotFoundException('Team Type', teamTypeId);
93+
}
94+
95+
const wbsElement = await prisma.wBS_Element.findUnique({
96+
where: {
97+
wbsNumber: {
98+
carNumber: wbsNum.carNumber,
99+
projectNumber: wbsNum.projectNumber,
100+
workPackageNumber: wbsNum.workPackageNumber
101+
}
102+
}
103+
});
104+
105+
if (!wbsElement) {
106+
throw new NotFoundException('WBS Element', wbsNum.carNumber);
107+
}
108+
109+
if (meetingTimes.length === 0) {
110+
throw new HttpException(400, 'There must be at least one meeting time');
111+
}
112+
113+
// checks if the meeting times are valid times and are all continous (ie. [1, 2, 3, 4])
114+
for (let i = 0; i < meetingTimes.length; i++) {
115+
if (i === meetingTimes.length - 1) {
116+
if (meetingTimes[i] < 0 || meetingTimes[i] > 83) {
117+
throw new HttpException(400, 'Meeting times have to be in range 0-83');
118+
}
119+
continue;
120+
}
121+
if (meetingTimes[i + 1] - meetingTimes[i] !== 1 || meetingTimes[i] < 0 || meetingTimes[i] > 83) {
122+
throw new HttpException(400, 'Meeting times have to be continous and in range 0-83');
123+
}
124+
}
125+
126+
if (isOnline && !zoomLink) {
127+
throw new HttpException(400, 'If the design review is online then there needs to be a zoom link');
128+
}
129+
130+
if (isInPerson && !location) {
131+
throw new HttpException(400, 'If the design review is in person then there needs to be a location');
132+
}
133+
134+
if (dateScheduled.valueOf() < new Date().valueOf()) {
135+
throw new HttpException(400, 'Design review cannot be scheduled for a past day');
136+
}
137+
138+
const designReview = await prisma.design_Review.create({
139+
data: {
140+
dateScheduled,
141+
dateCreated: new Date(),
142+
status: Design_Review_Status.UNCONFIRMED,
143+
location,
144+
isOnline,
145+
isInPerson,
146+
zoomLink,
147+
docTemplateLink,
148+
userCreated: { connect: { userId: submitter.userId } },
149+
teamType: { connect: { teamTypeId: teamType.teamTypeId } },
150+
requiredMembers: { connect: requiredMemberIds.map((memberId) => ({ userId: memberId })) },
151+
optionalMembers: { connect: optionalMemberIds.map((memberId) => ({ userId: memberId })) },
152+
meetingTimes,
153+
wbsElement: { connect: { wbsElementId: wbsElement.wbsElementId } }
154+
},
155+
...designReviewQueryArgs
156+
});
157+
158+
const members = await prisma.user.findMany({
159+
where: { userId: { in: optionalMemberIds.concat(requiredMemberIds) } }
160+
});
161+
162+
if (!members) {
163+
throw new NotFoundException('User', 'Cannot find members who are invited to the design review');
164+
}
165+
166+
// get the user settings for all the members invited, who are leaderingship
167+
const memberUserSettings = await prisma.user_Settings.findMany({
168+
where: { userId: { in: members.map((member) => member.userId) } }
169+
});
170+
171+
if (!memberUserSettings) {
172+
throw new NotFoundException('User Settings', 'Cannot find settings of members');
173+
}
174+
175+
// send a slack message to all leadership invited to the design review
176+
for (const memberUserSetting of memberUserSettings) {
177+
if (memberUserSetting.slackId) {
178+
try {
179+
await sendSlackDesignReviewNotification(memberUserSetting.slackId, designReview.designReviewId);
180+
} catch (err: unknown) {
181+
if (err instanceof Error) {
182+
throw new HttpException(500, `Failed to send slack notification: ${err.message}`);
183+
}
184+
}
185+
}
186+
}
187+
188+
return designReviewTransformer(designReview);
189+
}
190+
48191
/**
49192
* Retrieves a single design review
50193
*

src/backend/src/utils/errors.utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,5 @@ type ExceptionObjectNames =
121121
| 'Unit'
122122
| 'Material'
123123
| 'Link Type'
124-
| 'Design Review';
124+
| 'Design Review'
125+
| 'Team Type';

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

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1-
import { prismaDesignReview1, prismaDesignReview2, sharedDesignReview1 } from './test-data/design-reviews.test-data';
2-
import { batman, wonderwoman } from './test-data/users.test-data';
1+
import {
2+
prismaDesignReview1,
3+
prismaDesignReview2,
4+
sharedDesignReview1,
5+
teamType1
6+
} from './test-data/design-reviews.test-data';
7+
import { batman, theVisitor, wonderwoman } from './test-data/users.test-data';
38
import DesignReviewService from '../src/services/design-review.services';
49
import prisma from '../src/prisma/prisma';
5-
import { AccessDeniedAdminOnlyException, DeletedException, NotFoundException } from '../src/utils/errors.utils';
10+
import {
11+
AccessDeniedAdminOnlyException,
12+
AccessDeniedException,
13+
DeletedException,
14+
NotFoundException
15+
} from '../src/utils/errors.utils';
16+
import { prismaWbsElement1 } from './test-data/wbs-element.test-data';
617

718
describe('Design Reviews', () => {
819
beforeEach(() => {});
@@ -91,4 +102,102 @@ describe('Design Reviews', () => {
91102
expect(result).toEqual(sharedDesignReview1);
92103
});
93104
});
105+
106+
describe('Create design review tests', () => {
107+
test('Create design review succeeds', async () => {
108+
vi.spyOn(prisma.teamType, 'findFirst').mockResolvedValue(teamType1);
109+
vi.spyOn(prisma.wBS_Element, 'findUnique').mockResolvedValue(prismaWbsElement1);
110+
vi.spyOn(prisma.design_Review, 'create').mockResolvedValue(prismaDesignReview1);
111+
112+
const res = await DesignReviewService.createDesignReview(
113+
batman,
114+
new Date('2024-03-25'),
115+
'1',
116+
[],
117+
[],
118+
true,
119+
false,
120+
'doc temp',
121+
{
122+
carNumber: 1,
123+
projectNumber: 2,
124+
workPackageNumber: 0
125+
},
126+
[0, 1, 2, 3],
127+
'zoooooom'
128+
);
129+
130+
expect(res.teamType).toBe(teamType1);
131+
});
132+
133+
test('Create design review fails guest permission', async () => {
134+
await expect(
135+
DesignReviewService.createDesignReview(
136+
theVisitor,
137+
new Date('2024-03-25'),
138+
'1',
139+
[],
140+
[],
141+
true,
142+
false,
143+
'doc temp',
144+
{
145+
carNumber: 1,
146+
projectNumber: 2,
147+
workPackageNumber: 0
148+
},
149+
[0, 1, 2, 3],
150+
'zoom'
151+
)
152+
).rejects.toThrow(new AccessDeniedException('create design review'));
153+
});
154+
155+
test('Create design review team type not found', async () => {
156+
vi.spyOn(prisma.teamType, 'findFirst').mockResolvedValue(null);
157+
await expect(
158+
DesignReviewService.createDesignReview(
159+
batman,
160+
new Date('2024-03-25'),
161+
'15',
162+
[],
163+
[],
164+
true,
165+
false,
166+
'doc temp',
167+
{
168+
carNumber: 1,
169+
projectNumber: 2,
170+
workPackageNumber: 0
171+
},
172+
[0, 1, 2, 3],
173+
'zoom'
174+
)
175+
).rejects.toThrow(new NotFoundException('Team Type', '15'));
176+
});
177+
178+
test('Create design review wbs element not found', async () => {
179+
vi.spyOn(prisma.teamType, 'findFirst').mockResolvedValue(teamType1);
180+
181+
vi.spyOn(prisma.wBS_Element, 'findUnique').mockResolvedValue(null);
182+
await expect(
183+
DesignReviewService.createDesignReview(
184+
batman,
185+
new Date('2024-03-25'),
186+
'1',
187+
[],
188+
[],
189+
true,
190+
false,
191+
'doc temp',
192+
{
193+
carNumber: 15,
194+
projectNumber: 2,
195+
workPackageNumber: 0
196+
},
197+
[0, 1, 2, 3],
198+
'zoom'
199+
)
200+
).rejects.toThrow(new NotFoundException('WBS Element', 15));
201+
});
202+
});
94203
});

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,14 @@ export const manufacturer2: Manufacturer = {
244244
materials: [material1]
245245
};
246246

247+
export const manufacturer3: Manufacturer = {
248+
name: 'Amazon',
249+
dateCreated: new Date('03-13-2024'),
250+
userCreatedId: 2,
251+
userCreated: superman,
252+
materials: [material1]
253+
};
254+
247255
export const toolMaterial: PrismaMaterialType = {
248256
name: 'NERSoftwareTools',
249257
dateCreated: new Date(),

0 commit comments

Comments
 (0)