From 3084cbec64cbf22e20649c33835ecd0147a5154f Mon Sep 17 00:00:00 2001 From: RA341 Date: Tue, 22 Oct 2024 20:44:14 -0400 Subject: [PATCH 1/6] added retrieve by email --- devU-api/src/entities/user/user.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/devU-api/src/entities/user/user.service.ts b/devU-api/src/entities/user/user.service.ts index fa2b584..b25bb9a 100644 --- a/devU-api/src/entities/user/user.service.ts +++ b/devU-api/src/entities/user/user.service.ts @@ -29,6 +29,10 @@ export async function retrieve(id: number) { return await connect().findOneBy({ id, deletedAt: IsNull() }) } +export async function retrieveByEmail(email: string) { + return await connect().findOneBy({ email: email, deletedAt: IsNull() }) +} + export async function list() { return await connect().findBy({ deletedAt: IsNull() }) } @@ -56,6 +60,7 @@ export async function ensure(userInfo: User) { export default { create, retrieve, + retrieveByEmail, update, _delete, list, From 30c45d9f9e965a3a30c67ed5758a70ecc73507dd Mon Sep 17 00:00:00 2001 From: RA341 Date: Tue, 22 Oct 2024 20:45:23 -0400 Subject: [PATCH 2/6] removed throwing error if user is already enrolled --- devU-api/src/entities/userCourse/userCourse.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/devU-api/src/entities/userCourse/userCourse.service.ts b/devU-api/src/entities/userCourse/userCourse.service.ts index 88baae8..ec7fdaa 100644 --- a/devU-api/src/entities/userCourse/userCourse.service.ts +++ b/devU-api/src/entities/userCourse/userCourse.service.ts @@ -10,7 +10,10 @@ const connect = () => dataSource.getRepository(UserCourse) export async function create(userCourse: UserCourseType) { const userId = userCourse.userId const hasEnrolled = await connect().findOneBy({ userId, courseId: userCourse.courseId }) - if (hasEnrolled) throw new Error('User already enrolled in course') + if (hasEnrolled) { + console.warn('User already enrolled in course') + return null + } return await connect().save(userCourse) } From d0ab2cb6129c432e747e2eaed59beda4eea815a7 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sat, 26 Oct 2024 13:14:48 -0400 Subject: [PATCH 3/6] added bulk add/drop endpoint --- .../userCourse/userCourse.controller.ts | 51 ++++++++- .../entities/userCourse/userCourse.router.ts | 100 +++++++++++++++++- .../entities/userCourse/userCourse.service.ts | 52 +++++++-- 3 files changed, 192 insertions(+), 11 deletions(-) diff --git a/devU-api/src/entities/userCourse/userCourse.controller.ts b/devU-api/src/entities/userCourse/userCourse.controller.ts index 4c98af8..82761ff 100644 --- a/devU-api/src/entities/userCourse/userCourse.controller.ts +++ b/devU-api/src/entities/userCourse/userCourse.controller.ts @@ -73,12 +73,16 @@ export async function detailByUser(req: Request, res: Response, next: NextFuncti export async function post(req: Request, res: Response, next: NextFunction) { try { const userCourse = await UserCourseService.create(req.body) + if (userCourse === null) { + return res.status(409).json('User already enrolled') + } + const response = serialize(userCourse) res.status(201).json(response) } catch (err) { if (err instanceof Error) { - res.status(400).json(new GenericResponse(err.message)) + res.status(400).json(new GenericResponse(err.message)) } } } @@ -88,7 +92,10 @@ export async function put(req: Request, res: Response, next: NextFunction) { req.body.courseId = parseInt(req.params.id) const currentUser = req.currentUser?.userId if (!currentUser) return res.status(401).json({ message: 'Unauthorized' }) - const results = await UserCourseService.update(req.body, currentUser) + + req.body.userId = currentUser + + const results = await UserCourseService.update(req.body) if (!results.affected) return res.status(404).json(NotFound) res.status(200).json(Updated) @@ -128,4 +135,42 @@ export async function _delete(req: Request, res: Response, next: NextFunction) { } } -export default { get, getByCourse, getAll, detail, detailByUser, post, put, _delete, checkEnroll } +export async function addStudents(req: Request, res: Response, next: NextFunction) { + try { + const userEmails = req.body['users'] as string[] + if (!userEmails || userEmails.length == 0) return res.status(422).json({ message: 'users field not found or is empty' }) + const courseId = parseInt(req.params.courseId) + + const result = await UserCourseService.bulkCreate(userEmails, courseId, false) + res.status(201).json(result) + } catch (err) { + next(err) + } +} + +export async function dropStudents(req: Request, res: Response, next: NextFunction) { + try { + const userEmails = req.body['users'] as string[] + if (!userEmails || userEmails.length == 0) return res.status(422).json({ message: 'users field not found or is empty' }) + const courseId = parseInt(req.params.courseId) + + const result = await UserCourseService.bulkCreate(userEmails, courseId, true) + res.status(201).json(result) + } catch (err) { + next(err) + } +} + +export default { + get, + getByCourse, + getAll, + detail, + detailByUser, + post, + put, + _delete, + checkEnroll, + addStudents, + dropStudents, +} diff --git a/devU-api/src/entities/userCourse/userCourse.router.ts b/devU-api/src/entities/userCourse/userCourse.router.ts index c781c49..1435886 100644 --- a/devU-api/src/entities/userCourse/userCourse.router.ts +++ b/devU-api/src/entities/userCourse/userCourse.router.ts @@ -80,7 +80,7 @@ Router.get( extractOwnerByPathParam('userId'), isAuthorized('courseViewAll', 'enrolled'), asInt('userId'), - UserCourseController.detailByUser + UserCourseController.detailByUser, ) /** @@ -102,6 +102,104 @@ Router.get( Router.post('/', validator, UserCourseController.post) // TODO: userCourseEditAll eventually. For now, allow self enroll + +/** + * @swagger + * /courses/{courseId}/users/add: + * put: + * summary: Add multiple students to a course + * tags: + * - UserCourses + * responses: + * 200: + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: string + * description: Array of successfully enrolled users as a string + * example: '["test@test1.com enrolled successfully"]' + * failed: + * type: string + * description: Array of failed enrollments with error messages as a string + * example: '["user@email.com: Error: User already enrolled in course", "user2@email.com not found"]' + * required: + * - success + * - failed + * parameters: + * - name: courseId + * in: path + * required: true + * schema: + * type: integer + * requestBody: + * description: A list of emails in a json array + * content: + * application/json: + * schema: + * type: object + * properties: + * users: + * type: array + * items: + * type: string + * format: email + * required: + * - users + */ +Router.post('/students/add', asInt('courseId'), isAuthorized('courseViewAll'), UserCourseController.addStudents) + +/** + * @swagger + * /courses/{courseId}/users/drop: + * put: + * summary: Drop multiple students from a course + * tags: + * - UserCourses + * responses: + * 200: + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: string + * description: Array of successfully dropped students as a string + * example: '["test@test1.com dropped successfully"]' + * failed: + * type: string + * description: Array of failed drops with error messages as a string + * example: '["user2@email.com not found"]' + * required: + * - success + * - failed + * parameters: + * - name: courseId + * in: path + * required: true + * schema: + * type: integer + * requestBody: + * description: A list of emails in a json array + * content: + * application/json: + * schema: + * type: object + * properties: + * users: + * type: array + * items: + * type: string + * format: email + * required: + * - users + */ +Router.post('/students/drop', asInt('courseId'), isAuthorized('courseViewAll'), UserCourseController.dropStudents) + + /** * @swagger * /course/:courseId/users-courses/{id}: diff --git a/devU-api/src/entities/userCourse/userCourse.service.ts b/devU-api/src/entities/userCourse/userCourse.service.ts index ec7fdaa..a9a8ff9 100644 --- a/devU-api/src/entities/userCourse/userCourse.service.ts +++ b/devU-api/src/entities/userCourse/userCourse.service.ts @@ -4,23 +4,59 @@ import { dataSource } from '../../database' import { UserCourse as UserCourseType } from 'devu-shared-modules' import UserCourse from './userCourse.model' +import UserService from '../user/user.service' const connect = () => dataSource.getRepository(UserCourse) export async function create(userCourse: UserCourseType) { const userId = userCourse.userId const hasEnrolled = await connect().findOneBy({ userId, courseId: userCourse.courseId }) - if (hasEnrolled) { - console.warn('User already enrolled in course') - return null - } + + if (hasEnrolled) throw new Error('User already enrolled in course') return await connect().save(userCourse) } -export async function update(userCourse: UserCourseType, currentUser: number) { - const { courseId, role, dropped } = userCourse +// Add/drop students based on a list of users, +// to drop students, set the third param to true +export async function bulkAddDrop(userEmails: string[], courseId: number, drop: boolean) { + const failed: string[] = [] + const success: string[] = [] + + for (const email of userEmails) { + const user = await UserService.retrieveByEmail(email) + if (user === null) { + failed.push(`${email} not found`) + continue + } + + const student: UserCourseType = { + userId: user.id, + role: 'student', + courseId: courseId, + dropped: drop, + } + + try { + if (!drop) { + await create(student) + success.push(`${email} enrolled successfully`) + } else { + await update(student) + success.push(`${email} dropped successfully`) + } + } catch (e) { + console.error(`Error occurred while bulk add/drop ${e}`) + failed.push(`${email}: ${e}`) + } + } + + return { 'success': JSON.stringify(success), 'failed': JSON.stringify(failed) } +} + +export async function update(userCourse: UserCourseType) { + const { courseId, role, dropped, userId } = userCourse if (!courseId) throw new Error('Missing course Id') - const userCourseData = await connect().findOneBy({ courseId, userId: currentUser }) + const userCourseData = await connect().findOneBy({ courseId, userId: userId }) if (!userCourseData) throw new Error('User not enrolled in course') userCourseData.role = role userCourseData.dropped = dropped @@ -46,6 +82,7 @@ export async function list(userId: number) { // TODO: look into/test this return await connect().findBy({ userId, deletedAt: IsNull() }) } + export async function listAll() { return await connect().findBy({ deletedAt: IsNull() }) } @@ -74,4 +111,5 @@ export default { listByCourse, listByUser, checking: checkIfEnrolled, + bulkCreate: bulkAddDrop, } From 049ce7f51e3f31d7ef5a13ec18a37a5eace4fef6 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sat, 26 Oct 2024 13:21:10 -0400 Subject: [PATCH 4/6] switched usercourse to usercoursedata --- devU-api/src/entities/userCourse/userCourse.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devU-api/src/entities/userCourse/userCourse.service.ts b/devU-api/src/entities/userCourse/userCourse.service.ts index a9a8ff9..4f3014c 100644 --- a/devU-api/src/entities/userCourse/userCourse.service.ts +++ b/devU-api/src/entities/userCourse/userCourse.service.ts @@ -60,8 +60,8 @@ export async function update(userCourse: UserCourseType) { if (!userCourseData) throw new Error('User not enrolled in course') userCourseData.role = role userCourseData.dropped = dropped - if (!userCourse.id) throw new Error('Missing Id') - return await connect().update(userCourse.id, userCourseData) + if (!userCourseData.id) throw new Error('Missing Id') + return await connect().update(userCourseData.id, userCourseData) } export async function _delete(courseId: number, userId: number) { From d642aacc621f21a2b4078ed5904e718de0c47f7a Mon Sep 17 00:00:00 2001 From: RA341 Date: Sat, 26 Oct 2024 13:31:35 -0400 Subject: [PATCH 5/6] added re-enroll handler --- .../src/entities/userCourse/userCourse.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/devU-api/src/entities/userCourse/userCourse.service.ts b/devU-api/src/entities/userCourse/userCourse.service.ts index 4f3014c..4fefa9c 100644 --- a/devU-api/src/entities/userCourse/userCourse.service.ts +++ b/devU-api/src/entities/userCourse/userCourse.service.ts @@ -38,7 +38,16 @@ export async function bulkAddDrop(userEmails: string[], courseId: number, drop: try { if (!drop) { - await create(student) + try { + await create(student) + } catch (error) { + if (error instanceof Error && error.message === 'User already enrolled in course') { + // update student drop to false, since they re-enrolled after being dropped + await update(student) + } else { + throw error; // re-throw if it's a different error + } + } success.push(`${email} enrolled successfully`) } else { await update(student) From 500a62dc00eb51a33aaf1ff4d86c65fae80890a2 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sat, 26 Oct 2024 13:35:02 -0400 Subject: [PATCH 6/6] fixed openapi doc --- devU-api/src/entities/userCourse/userCourse.router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devU-api/src/entities/userCourse/userCourse.router.ts b/devU-api/src/entities/userCourse/userCourse.router.ts index 1435886..f147f2d 100644 --- a/devU-api/src/entities/userCourse/userCourse.router.ts +++ b/devU-api/src/entities/userCourse/userCourse.router.ts @@ -105,7 +105,7 @@ Router.post('/', validator, UserCourseController.post) /** * @swagger - * /courses/{courseId}/users/add: + * /courses/{courseId}/students/add: * put: * summary: Add multiple students to a course * tags: @@ -153,7 +153,7 @@ Router.post('/students/add', asInt('courseId'), isAuthorized('courseViewAll'), U /** * @swagger - * /courses/{courseId}/users/drop: + * /courses/{courseId}/students/drop: * put: * summary: Drop multiple students from a course * tags: