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, 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..f147f2d 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}/students/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}/students/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 88baae8..4fefa9c 100644 --- a/devU-api/src/entities/userCourse/userCourse.service.ts +++ b/devU-api/src/entities/userCourse/userCourse.service.ts @@ -4,25 +4,73 @@ 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) 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) { + 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) + 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 - 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) { @@ -43,6 +91,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() }) } @@ -71,4 +120,5 @@ export default { listByCourse, listByUser, checking: checkIfEnrolled, + bulkCreate: bulkAddDrop, }