Skip to content

Commit

Permalink
Merge pull request #158 from RA341/add-users
Browse files Browse the repository at this point in the history
added bulk enroll/drop students
  • Loading branch information
jessehartloff authored Oct 28, 2024
2 parents 29da364 + 500a62d commit a61de37
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 9 deletions.
5 changes: 5 additions & 0 deletions devU-api/src/entities/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() })
}
Expand Down Expand Up @@ -56,6 +60,7 @@ export async function ensure(userInfo: User) {
export default {
create,
retrieve,
retrieveByEmail,
update,
_delete,
list,
Expand Down
51 changes: 48 additions & 3 deletions devU-api/src/entities/userCourse/userCourse.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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,
}
100 changes: 99 additions & 1 deletion devU-api/src/entities/userCourse/userCourse.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Router.get(
extractOwnerByPathParam('userId'),
isAuthorized('courseViewAll', 'enrolled'),
asInt('userId'),
UserCourseController.detailByUser
UserCourseController.detailByUser,
)

/**
Expand All @@ -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: '["[email protected] enrolled successfully"]'
* failed:
* type: string
* description: Array of failed enrollments with error messages as a string
* example: '["[email protected]: Error: User already enrolled in course", "[email protected] 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: '["[email protected] dropped successfully"]'
* failed:
* type: string
* description: Array of failed drops with error messages as a string
* example: '["[email protected] 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}:
Expand Down
60 changes: 55 additions & 5 deletions devU-api/src/entities/userCourse/userCourse.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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() })
}
Expand Down Expand Up @@ -71,4 +120,5 @@ export default {
listByCourse,
listByUser,
checking: checkIfEnrolled,
bulkCreate: bulkAddDrop,
}

0 comments on commit a61de37

Please sign in to comment.