Merge pull request #142 from SELab-2/feat/user-routes
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Lint / Run linters (push) Has been cancelled
				
					
					
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Lint / Run linters (push) Has been cancelled
				feat: endpoints finaliseren users
This commit is contained in:
		
						commit
						2b2c97a82d
					
				
					 32 changed files with 1443 additions and 374 deletions
				
			
		
							
								
								
									
										18
									
								
								backend/src/controllers/error-helper.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								backend/src/controllers/error-helper.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Checks for the presence of required fields and throws a BadRequestException | ||||||
|  |  * if any are missing. | ||||||
|  |  * | ||||||
|  |  * @param fields - An object with key-value pairs to validate. | ||||||
|  |  */ | ||||||
|  | export function requireFields(fields: Record<string, unknown>): void { | ||||||
|  |     const missing = Object.entries(fields) | ||||||
|  |         .filter(([_, value]) => value === undefined || value === null || value === '') | ||||||
|  |         .map(([key]) => key); | ||||||
|  | 
 | ||||||
|  |     if (missing.length > 0) { | ||||||
|  |         const message = `Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`; | ||||||
|  |         throw new BadRequestException(message); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,100 +1,67 @@ | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| import { | import { | ||||||
|  |     createClassJoinRequest, | ||||||
|     createStudent, |     createStudent, | ||||||
|  |     deleteClassJoinRequest, | ||||||
|     deleteStudent, |     deleteStudent, | ||||||
|     getAllStudents, |     getAllStudents, | ||||||
|  |     getJoinRequestByStudentClass, | ||||||
|  |     getJoinRequestsByStudent, | ||||||
|     getStudent, |     getStudent, | ||||||
|     getStudentAssignments, |     getStudentAssignments, | ||||||
|     getStudentClasses, |     getStudentClasses, | ||||||
|     getStudentGroups, |     getStudentGroups, | ||||||
|  |     getStudentQuestions, | ||||||
|     getStudentSubmissions, |     getStudentSubmissions, | ||||||
| } from '../services/students.js'; | } from '../services/students.js'; | ||||||
| 
 | import { requireFields } from './error-helper.js'; | ||||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||||
| 
 | 
 | ||||||
| // TODO: accept arguments (full, ...)
 |  | ||||||
| // TODO: endpoints
 |  | ||||||
| export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> { | export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     const students = await getAllStudents(full); |     const students: StudentDTO[] | string[] = await getAllStudents(full); | ||||||
| 
 | 
 | ||||||
|     if (!students) { |     res.json({ students }); | ||||||
|         res.status(404).json({ error: `Student not found.` }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.json({ students: students }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentHandler(req: Request, res: Response): Promise<void> { | export async function getStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username; |     const username = req.params.username; | ||||||
|  |     requireFields({ username }); | ||||||
| 
 | 
 | ||||||
|     if (!username) { |     const student = await getStudent(username); | ||||||
|         res.status(400).json({ error: 'Missing required field: username' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const user = await getStudent(username); |     res.json({ student }); | ||||||
| 
 |  | ||||||
|     if (!user) { |  | ||||||
|         res.status(404).json({ |  | ||||||
|             error: `User with username '${username}' not found.`, |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.json(user); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createStudentHandler(req: Request, res: Response): Promise<void> { | export async function createStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const username = req.body.username; | ||||||
|  |     const firstName = req.body.firstName; | ||||||
|  |     const lastName = req.body.lastName; | ||||||
|  |     requireFields({ username, firstName, lastName }); | ||||||
|  | 
 | ||||||
|     const userData = req.body as StudentDTO; |     const userData = req.body as StudentDTO; | ||||||
| 
 | 
 | ||||||
|     if (!userData.username || !userData.firstName || !userData.lastName) { |     const student = await createStudent(userData); | ||||||
|         res.status(400).json({ |     res.json({ student }); | ||||||
|             error: 'Missing required fields: username, firstName, lastName', |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const newUser = await createStudent(userData); |  | ||||||
| 
 |  | ||||||
|     if (!newUser) { |  | ||||||
|         res.status(500).json({ |  | ||||||
|             error: 'Something went wrong while creating student', |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.status(201).json(newUser); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteStudentHandler(req: Request, res: Response): Promise<void> { | export async function deleteStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username; |     const username = req.params.username; | ||||||
|  |     requireFields({ username }); | ||||||
| 
 | 
 | ||||||
|     if (!username) { |     const student = await deleteStudent(username); | ||||||
|         res.status(400).json({ error: 'Missing required field: username' }); |     res.json({ student }); | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const deletedUser = await deleteStudent(username); |  | ||||||
|     if (!deletedUser) { |  | ||||||
|         res.status(404).json({ |  | ||||||
|             error: `User with username '${username}' not found.`, |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.status(200).json(deletedUser); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> { | export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|     const username = req.params.id; |     const username = req.params.username; | ||||||
|  |     requireFields({ username }); | ||||||
| 
 | 
 | ||||||
|     const classes = await getStudentClasses(username, full); |     const classes = await getStudentClasses(username, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ classes: classes }); |     res.json({ classes }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO
 | // TODO
 | ||||||
|  | @ -103,33 +70,75 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro | ||||||
| // Have this assignment.
 | // Have this assignment.
 | ||||||
| export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> { | export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|     const username = req.params.id; |     const username = req.params.username; | ||||||
|  |     requireFields({ username }); | ||||||
| 
 | 
 | ||||||
|     const assignments = getStudentAssignments(username, full); |     const assignments = getStudentAssignments(username, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ assignments }); | ||||||
|         assignments: assignments, |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> { | export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|     const username = req.params.id; |     const username = req.params.username; | ||||||
|  |     requireFields({ username }); | ||||||
| 
 | 
 | ||||||
|     const groups = await getStudentGroups(username, full); |     const groups = await getStudentGroups(username, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ groups }); | ||||||
|         groups: groups, |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> { | export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.id; |     const username = req.params.username; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|  |     requireFields({ username }); | ||||||
| 
 | 
 | ||||||
|     const submissions = await getStudentSubmissions(username, full); |     const submissions = await getStudentSubmissions(username, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ submissions }); | ||||||
|         submissions: submissions, | } | ||||||
|     }); | 
 | ||||||
|  | export async function getStudentQuestionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const full = req.query.full === 'true'; | ||||||
|  |     const username = req.params.username; | ||||||
|  |     requireFields({ username }); | ||||||
|  | 
 | ||||||
|  |     const questions = await getStudentQuestions(username, full); | ||||||
|  | 
 | ||||||
|  |     res.json({ questions }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createStudentRequestHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const username = req.params.username; | ||||||
|  |     const classId = req.body.classId; | ||||||
|  |     requireFields({ username, classId }); | ||||||
|  | 
 | ||||||
|  |     const request = await createClassJoinRequest(username, classId); | ||||||
|  |     res.json({ request }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getStudentRequestsHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const username = req.params.username; | ||||||
|  |     requireFields({ username }); | ||||||
|  | 
 | ||||||
|  |     const requests = await getJoinRequestsByStudent(username); | ||||||
|  |     res.json({ requests }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getStudentRequestHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const username = req.params.username; | ||||||
|  |     const classId = req.params.classId; | ||||||
|  |     requireFields({ username, classId }); | ||||||
|  | 
 | ||||||
|  |     const request = await getJoinRequestByStudentClass(username, classId); | ||||||
|  |     res.json({ request }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteClassJoinRequestHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const username = req.params.username; | ||||||
|  |     const classId = req.params.classId; | ||||||
|  |     requireFields({ username, classId }); | ||||||
|  | 
 | ||||||
|  |     const request = await deleteClassJoinRequest(username, classId); | ||||||
|  |     res.json({ request }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,137 +4,97 @@ import { | ||||||
|     deleteTeacher, |     deleteTeacher, | ||||||
|     getAllTeachers, |     getAllTeachers, | ||||||
|     getClassesByTeacher, |     getClassesByTeacher, | ||||||
|     getQuestionsByTeacher, |     getJoinRequestsByClass, | ||||||
|     getStudentsByTeacher, |     getStudentsByTeacher, | ||||||
|     getTeacher, |     getTeacher, | ||||||
|  |     getTeacherQuestions, | ||||||
|  |     updateClassJoinRequestStatus, | ||||||
| } from '../services/teachers.js'; | } from '../services/teachers.js'; | ||||||
|  | import { requireFields } from './error-helper.js'; | ||||||
| import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | ||||||
| 
 | 
 | ||||||
| export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> { | export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     const teachers = await getAllTeachers(full); |     const teachers: TeacherDTO[] | string[] = await getAllTeachers(full); | ||||||
| 
 | 
 | ||||||
|     if (!teachers) { |     res.json({ teachers }); | ||||||
|         res.status(404).json({ error: `Teacher not found.` }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.json({ teachers: teachers }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username; |     const username = req.params.username; | ||||||
|  |     requireFields({ username }); | ||||||
| 
 | 
 | ||||||
|     if (!username) { |     const teacher = await getTeacher(username); | ||||||
|         res.status(400).json({ error: 'Missing required field: username' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const user = await getTeacher(username); |     res.json({ teacher }); | ||||||
| 
 |  | ||||||
|     if (!user) { |  | ||||||
|         res.status(404).json({ |  | ||||||
|             error: `Teacher '${username}' not found.`, |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.json(user); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createTeacherHandler(req: Request, res: Response): Promise<void> { | export async function createTeacherHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const username = req.body.username; | ||||||
|  |     const firstName = req.body.firstName; | ||||||
|  |     const lastName = req.body.lastName; | ||||||
|  |     requireFields({ username, firstName, lastName }); | ||||||
|  | 
 | ||||||
|     const userData = req.body as TeacherDTO; |     const userData = req.body as TeacherDTO; | ||||||
| 
 | 
 | ||||||
|     if (!userData.username || !userData.firstName || !userData.lastName) { |     const teacher = await createTeacher(userData); | ||||||
|         res.status(400).json({ |     res.json({ teacher }); | ||||||
|             error: 'Missing required fields: username, firstName, lastName', |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const newUser = await createTeacher(userData); |  | ||||||
| 
 |  | ||||||
|     if (!newUser) { |  | ||||||
|         res.status(400).json({ error: 'Failed to create teacher' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.status(201).json(newUser); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> { | export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username; |     const username = req.params.username; | ||||||
|  |     requireFields({ username }); | ||||||
| 
 | 
 | ||||||
|     if (!username) { |     const teacher = await deleteTeacher(username); | ||||||
|         res.status(400).json({ error: 'Missing required field: username' }); |     res.json({ teacher }); | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const deletedUser = await deleteTeacher(username); |  | ||||||
|     if (!deletedUser) { |  | ||||||
|         res.status(404).json({ |  | ||||||
|             error: `User '${username}' not found.`, |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.status(200).json(deletedUser); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username; |     const username = req.params.username; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 |     requireFields({ username }); | ||||||
|     if (!username) { |  | ||||||
|         res.status(400).json({ error: 'Missing required field: username' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const classes = await getClassesByTeacher(username, full); |     const classes = await getClassesByTeacher(username, full); | ||||||
| 
 | 
 | ||||||
|     if (!classes) { |     res.json({ classes }); | ||||||
|         res.status(404).json({ error: 'Teacher not found' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.json({ classes: classes }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username; |     const username = req.params.username; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 |     requireFields({ username }); | ||||||
|     if (!username) { |  | ||||||
|         res.status(400).json({ error: 'Missing required field: username' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const students = await getStudentsByTeacher(username, full); |     const students = await getStudentsByTeacher(username, full); | ||||||
| 
 | 
 | ||||||
|     if (!students) { |     res.json({ students }); | ||||||
|         res.status(404).json({ error: 'Teacher not found' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.json({ students: students }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username; |     const username = req.params.username; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|  |     requireFields({ username }); | ||||||
| 
 | 
 | ||||||
|     if (!username) { |     const questions = await getTeacherQuestions(username, full); | ||||||
|         res.status(400).json({ error: 'Missing required field: username' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const questions = await getQuestionsByTeacher(username, full); |     res.json({ questions }); | ||||||
| 
 | } | ||||||
|     if (!questions) { | 
 | ||||||
|         res.status(404).json({ error: 'Teacher not found' }); | export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> { | ||||||
|         return; |     const username = req.query.username as string; | ||||||
|     } |     const classId = req.params.classId; | ||||||
| 
 |     requireFields({ username, classId }); | ||||||
|     res.json({ questions: questions }); | 
 | ||||||
|  |     const joinRequests = await getJoinRequestsByClass(classId); | ||||||
|  |     res.json({ joinRequests }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const studentUsername = req.query.studentUsername as string; | ||||||
|  |     const classId = req.params.classId; | ||||||
|  |     const accepted = req.body.accepted !== 'false'; // Default = true
 | ||||||
|  |     requireFields({ studentUsername, classId }); | ||||||
|  | 
 | ||||||
|  |     const request = await updateClassJoinRequestStatus(studentUsername, classId, accepted); | ||||||
|  |     res.json({ request }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,13 +2,17 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| import { Class } from '../../entities/classes/class.entity.js'; | import { Class } from '../../entities/classes/class.entity.js'; | ||||||
| import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js'; | import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js'; | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | import { Student } from '../../entities/users/student.entity.js'; | ||||||
|  | import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; | ||||||
| 
 | 
 | ||||||
| export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> { | export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> { | ||||||
|     public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> { |     public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> { | ||||||
|         return this.findAll({ where: { requester: requester } }); |         return this.findAll({ where: { requester: requester } }); | ||||||
|     } |     } | ||||||
|     public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { |     public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { | ||||||
|         return this.findAll({ where: { class: clazz } }); |         return this.findAll({ where: { class: clazz, status: ClassJoinRequestStatus.Open } }); // TODO check if works like this
 | ||||||
|  |     } | ||||||
|  |     public async findByStudentAndClass(requester: Student, clazz: Class): Promise<ClassJoinRequest | null> { | ||||||
|  |         return this.findOne({ requester, class: clazz }); | ||||||
|     } |     } | ||||||
|     public async deleteBy(requester: Student, clazz: Class): Promise<void> { |     public async deleteBy(requester: Student, clazz: Class): Promise<void> { | ||||||
|         return this.deleteWhere({ requester: requester, class: clazz }); |         return this.deleteWhere({ requester: requester, class: clazz }); | ||||||
|  |  | ||||||
|  | @ -54,4 +54,11 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|             orderBy: { timestamp: 'ASC' }, |             orderBy: { timestamp: 'ASC' }, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public async findAllByAuthor(author: Student): Promise<Question[]> { | ||||||
|  |         return this.findAll({ | ||||||
|  |             where: { author }, | ||||||
|  |             orderBy: { timestamp: 'DESC' }, // New to old
 | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,4 @@ | ||||||
| export interface Theme { | import { Theme } from '@dwengo-1/common/interfaces/theme'; | ||||||
|     title: string; |  | ||||||
|     hruids: string[]; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export const themes: Theme[] = [ | export const themes: Theme[] = [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { mapToUserDTO } from './user.js'; | import { mapToUserDTO } from './user.js'; | ||||||
| import { mapToQuestionDTO, mapToQuestionId } from './question.js'; | import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; | ||||||
| import { Answer } from '../entities/questions/answer.entity.js'; | import { Answer } from '../entities/questions/answer.entity.js'; | ||||||
| import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; | import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; | ||||||
| 
 | 
 | ||||||
|  | @ -16,10 +16,10 @@ export function mapToAnswerDTO(answer: Answer): AnswerDTO { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToAnswerId(answer: AnswerDTO): AnswerId { | export function mapToAnswerDTOId(answer: Answer): AnswerId { | ||||||
|     return { |     return { | ||||||
|         author: answer.author.username, |         author: answer.author.username, | ||||||
|         toQuestion: mapToQuestionId(answer.toQuestion), |         toQuestion: mapToQuestionDTOId(answer.toQuestion), | ||||||
|         sequenceNumber: answer.sequenceNumber, |         sequenceNumber: answer.sequenceNumber!, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,16 +1,21 @@ | ||||||
| import { Question } from '../entities/questions/question.entity.js'; | import { Question } from '../entities/questions/question.entity.js'; | ||||||
| import { mapToStudentDTO } from './student.js'; | import { mapToStudentDTO } from './student.js'; | ||||||
| import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||||
|  | import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
|  | 
 | ||||||
|  | function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { | ||||||
|  |     return { | ||||||
|  |         hruid: question.learningObjectHruid, | ||||||
|  |         language: question.learningObjectLanguage, | ||||||
|  |         version: question.learningObjectVersion, | ||||||
|  |     }; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Convert a Question entity to a DTO format. |  * Convert a Question entity to a DTO format. | ||||||
|  */ |  */ | ||||||
| export function mapToQuestionDTO(question: Question): QuestionDTO { | export function mapToQuestionDTO(question: Question): QuestionDTO { | ||||||
|     const learningObjectIdentifier = { |     const learningObjectIdentifier = getLearningObjectIdentifier(question); | ||||||
|         hruid: question.learningObjectHruid, |  | ||||||
|         language: question.learningObjectLanguage, |  | ||||||
|         version: question.learningObjectVersion, |  | ||||||
|     }; |  | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         learningObjectIdentifier, |         learningObjectIdentifier, | ||||||
|  | @ -21,9 +26,11 @@ export function mapToQuestionDTO(question: Question): QuestionDTO { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToQuestionId(question: QuestionDTO): QuestionId { | export function mapToQuestionDTOId(question: Question): QuestionId { | ||||||
|  |     const learningObjectIdentifier = getLearningObjectIdentifier(question); | ||||||
|  | 
 | ||||||
|     return { |     return { | ||||||
|         learningObjectIdentifier: question.learningObjectIdentifier, |         learningObjectIdentifier, | ||||||
|         sequenceNumber: question.sequenceNumber!, |         sequenceNumber: question.sequenceNumber!, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										23
									
								
								backend/src/interfaces/student-request.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								backend/src/interfaces/student-request.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | ||||||
|  | import { mapToStudentDTO } from './student.js'; | ||||||
|  | import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js'; | ||||||
|  | import { getClassJoinRequestRepository } from '../data/repositories.js'; | ||||||
|  | import { Student } from '../entities/users/student.entity.js'; | ||||||
|  | import { Class } from '../entities/classes/class.entity.js'; | ||||||
|  | import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; | ||||||
|  | import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; | ||||||
|  | 
 | ||||||
|  | export function mapToStudentRequestDTO(request: ClassJoinRequest): ClassJoinRequestDTO { | ||||||
|  |     return { | ||||||
|  |         requester: mapToStudentDTO(request.requester), | ||||||
|  |         class: request.class.classId!, | ||||||
|  |         status: request.status, | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function mapToStudentRequest(student: Student, cls: Class): ClassJoinRequest { | ||||||
|  |     return getClassJoinRequestRepository().create({ | ||||||
|  |         requester: student, | ||||||
|  |         class: cls, | ||||||
|  |         status: ClassJoinRequestStatus.Open, | ||||||
|  |     }); | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								backend/src/routes/student-join-requests.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								backend/src/routes/student-join-requests.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | import express from 'express'; | ||||||
|  | import { | ||||||
|  |     createStudentRequestHandler, | ||||||
|  |     deleteClassJoinRequestHandler, | ||||||
|  |     getStudentRequestHandler, | ||||||
|  |     getStudentRequestsHandler, | ||||||
|  | } from '../controllers/students.js'; | ||||||
|  | 
 | ||||||
|  | const router = express.Router({ mergeParams: true }); | ||||||
|  | 
 | ||||||
|  | router.get('/', getStudentRequestsHandler); | ||||||
|  | 
 | ||||||
|  | router.post('/', createStudentRequestHandler); | ||||||
|  | 
 | ||||||
|  | router.get('/:classId', getStudentRequestHandler); | ||||||
|  | 
 | ||||||
|  | router.delete('/:classId', deleteClassJoinRequestHandler); | ||||||
|  | 
 | ||||||
|  | export default router; | ||||||
|  | @ -7,8 +7,10 @@ import { | ||||||
|     getStudentClassesHandler, |     getStudentClassesHandler, | ||||||
|     getStudentGroupsHandler, |     getStudentGroupsHandler, | ||||||
|     getStudentHandler, |     getStudentHandler, | ||||||
|  |     getStudentQuestionsHandler, | ||||||
|     getStudentSubmissionsHandler, |     getStudentSubmissionsHandler, | ||||||
| } from '../controllers/students.js'; | } from '../controllers/students.js'; | ||||||
|  | import joinRequestRouter from './student-join-requests.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | @ -17,30 +19,26 @@ router.get('/', getAllStudentsHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/', createStudentHandler); | router.post('/', createStudentHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/', deleteStudentHandler); |  | ||||||
| 
 |  | ||||||
| router.delete('/:username', deleteStudentHandler); | router.delete('/:username', deleteStudentHandler); | ||||||
| 
 | 
 | ||||||
| // Information about a student's profile
 | // Information about a student's profile
 | ||||||
| router.get('/:username', getStudentHandler); | router.get('/:username', getStudentHandler); | ||||||
| 
 | 
 | ||||||
| // The list of classes a student is in
 | // The list of classes a student is in
 | ||||||
| router.get('/:id/classes', getStudentClassesHandler); | router.get('/:username/classes', getStudentClassesHandler); | ||||||
| 
 | 
 | ||||||
| // The list of submissions a student has made
 | // The list of submissions a student has made
 | ||||||
| router.get('/:id/submissions', getStudentSubmissionsHandler); | router.get('/:username/submissions', getStudentSubmissionsHandler); | ||||||
| 
 | 
 | ||||||
| // The list of assignments a student has
 | // The list of assignments a student has
 | ||||||
| router.get('/:id/assignments', getStudentAssignmentsHandler); | router.get('/:username/assignments', getStudentAssignmentsHandler); | ||||||
| 
 | 
 | ||||||
| // The list of groups a student is in
 | // The list of groups a student is in
 | ||||||
| router.get('/:id/groups', getStudentGroupsHandler); | router.get('/:username/groups', getStudentGroupsHandler); | ||||||
| 
 | 
 | ||||||
| // A list of questions a user has created
 | // A list of questions a user has created
 | ||||||
| router.get('/:id/questions', (_req, res) => { | router.get('/:username/questions', getStudentQuestionsHandler); | ||||||
|     res.json({ | 
 | ||||||
|         questions: ['0'], | router.use('/:username/joinRequests', joinRequestRouter); | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -3,10 +3,12 @@ import { | ||||||
|     createTeacherHandler, |     createTeacherHandler, | ||||||
|     deleteTeacherHandler, |     deleteTeacherHandler, | ||||||
|     getAllTeachersHandler, |     getAllTeachersHandler, | ||||||
|  |     getStudentJoinRequestHandler, | ||||||
|     getTeacherClassHandler, |     getTeacherClassHandler, | ||||||
|     getTeacherHandler, |     getTeacherHandler, | ||||||
|     getTeacherQuestionHandler, |     getTeacherQuestionHandler, | ||||||
|     getTeacherStudentHandler, |     getTeacherStudentHandler, | ||||||
|  |     updateStudentJoinRequestHandler, | ||||||
| } from '../controllers/teachers.js'; | } from '../controllers/teachers.js'; | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | @ -15,8 +17,6 @@ router.get('/', getAllTeachersHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/', createTeacherHandler); | router.post('/', createTeacherHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/', deleteTeacherHandler); |  | ||||||
| 
 |  | ||||||
| router.get('/:username', getTeacherHandler); | router.get('/:username', getTeacherHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:username', deleteTeacherHandler); | router.delete('/:username', deleteTeacherHandler); | ||||||
|  | @ -27,6 +27,10 @@ router.get('/:username/students', getTeacherStudentHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:username/questions', getTeacherQuestionHandler); | router.get('/:username/questions', getTeacherQuestionHandler); | ||||||
| 
 | 
 | ||||||
|  | router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); | ||||||
|  | 
 | ||||||
|  | router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); | ||||||
|  | 
 | ||||||
| // Invitations to other classes a teacher received
 | // Invitations to other classes a teacher received
 | ||||||
| router.get('/:id/invitations', (_req, res) => { | router.get('/:id/invitations', (_req, res) => { | ||||||
|     res.json({ |     res.json({ | ||||||
|  |  | ||||||
|  | @ -3,12 +3,25 @@ import { mapToClassDTO } from '../interfaces/class.js'; | ||||||
| import { mapToStudentDTO } from '../interfaces/student.js'; | import { mapToStudentDTO } from '../interfaces/student.js'; | ||||||
| import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.js'; | import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.js'; | ||||||
| import { getLogger } from '../logging/initalize.js'; | import { getLogger } from '../logging/initalize.js'; | ||||||
|  | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
|  | import { Class } from '../entities/classes/class.entity.js'; | ||||||
| import { ClassDTO } from '@dwengo-1/common/interfaces/class'; | import { ClassDTO } from '@dwengo-1/common/interfaces/class'; | ||||||
| import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; | import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; | ||||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||||
| 
 | 
 | ||||||
| const logger = getLogger(); | const logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|  | export async function fetchClass(classId: string): Promise<Class> { | ||||||
|  |     const classRepository = getClassRepository(); | ||||||
|  |     const cls = await classRepository.findById(classId); | ||||||
|  | 
 | ||||||
|  |     if (!cls) { | ||||||
|  |         throw new NotFoundException('Class with id not found'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return cls; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[]> { | export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[]> { | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|     const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); |     const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js'; | import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js'; | ||||||
| import { mapToQuestionDTO, mapToQuestionId } from '../interfaces/question.js'; | import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; | ||||||
| import { Question } from '../entities/questions/question.entity.js'; | import { Question } from '../entities/questions/question.entity.js'; | ||||||
| import { Answer } from '../entities/questions/answer.entity.js'; | import { Answer } from '../entities/questions/answer.entity.js'; | ||||||
| import { mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js'; | import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js'; | ||||||
| import { QuestionRepository } from '../data/questions/question-repository.js'; | import { QuestionRepository } from '../data/questions/question-repository.js'; | ||||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
| import { mapToStudent } from '../interfaces/student.js'; | import { mapToStudent } from '../interfaces/student.js'; | ||||||
|  | @ -17,13 +17,11 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea | ||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const questionsDTO: QuestionDTO[] = questions.map(mapToQuestionDTO); |  | ||||||
| 
 |  | ||||||
|     if (full) { |     if (full) { | ||||||
|         return questionsDTO; |         return questions.map(mapToQuestionDTO); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return questionsDTO.map(mapToQuestionId); |     return questions.map(mapToQuestionDTOId); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function fetchQuestion(questionId: QuestionId): Promise<Question | null> { | async function fetchQuestion(questionId: QuestionId): Promise<Question | null> { | ||||||
|  | @ -61,13 +59,11 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean | ||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const answersDTO = answers.map(mapToAnswerDTO); |  | ||||||
| 
 |  | ||||||
|     if (full) { |     if (full) { | ||||||
|         return answersDTO; |         return answers.map(mapToAnswerDTO); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return answersDTO.map(mapToAnswerId); |     return answers.map(mapToAnswerDTOId); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createQuestion(questionDTO: QuestionDTO): Promise<QuestionDTO | null> { | export async function createQuestion(questionDTO: QuestionDTO): Promise<QuestionDTO | null> { | ||||||
|  |  | ||||||
|  | @ -1,67 +1,75 @@ | ||||||
| import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; | import { | ||||||
|  |     getClassJoinRequestRepository, | ||||||
|  |     getClassRepository, | ||||||
|  |     getGroupRepository, | ||||||
|  |     getQuestionRepository, | ||||||
|  |     getStudentRepository, | ||||||
|  |     getSubmissionRepository, | ||||||
|  | } from '../data/repositories.js'; | ||||||
| import { mapToClassDTO } from '../interfaces/class.js'; | import { mapToClassDTO } from '../interfaces/class.js'; | ||||||
| import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | ||||||
| import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js'; | import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js'; | ||||||
| import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; | import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; | ||||||
| import { getAllAssignments } from './assignments.js'; | import { getAllAssignments } from './assignments.js'; | ||||||
| import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; | import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; | ||||||
|  | import { mapToStudentRequest, mapToStudentRequestDTO } from '../interfaces/student-request.js'; | ||||||
|  | import { Student } from '../entities/users/student.entity.js'; | ||||||
|  | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
|  | import { fetchClass } from './classes.js'; | ||||||
|  | import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||||
| import { ClassDTO } from '@dwengo-1/common/interfaces/class'; | import { ClassDTO } from '@dwengo-1/common/interfaces/class'; | ||||||
|  | import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; | ||||||
| import { GroupDTO } from '@dwengo-1/common/interfaces/group'; | import { GroupDTO } from '@dwengo-1/common/interfaces/group'; | ||||||
| import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; | import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; | ||||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||||
| import { getLogger } from '../logging/initalize.js'; | import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; | ||||||
| 
 | 
 | ||||||
| export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> { | export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
|     const students = await studentRepository.findAll(); |     const users = await studentRepository.findAll(); | ||||||
| 
 | 
 | ||||||
|     if (full) { |     if (full) { | ||||||
|         return students.map(mapToStudentDTO); |         return users.map(mapToStudentDTO); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return students.map((student) => student.username); |     return users.map((user) => user.username); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudent(username: string): Promise<StudentDTO | null> { | export async function fetchStudent(username: string): Promise<Student> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
|     const user = await studentRepository.findByUsername(username); |     const user = await studentRepository.findByUsername(username); | ||||||
|     return user ? mapToStudentDTO(user) : null; | 
 | ||||||
|  |     if (!user) { | ||||||
|  |         throw new NotFoundException('Student with username not found'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return user; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> { | export async function getStudent(username: string): Promise<StudentDTO> { | ||||||
|  |     const user = await fetchStudent(username); | ||||||
|  |     return mapToStudentDTO(user); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createStudent(userData: StudentDTO): Promise<StudentDTO> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
| 
 | 
 | ||||||
|     const newStudent = mapToStudent(userData); |     const newStudent = mapToStudent(userData); | ||||||
|     await studentRepository.save(newStudent, { preventOverwrite: true }); |     await studentRepository.save(newStudent, { preventOverwrite: true }); | ||||||
|     return mapToStudentDTO(newStudent); |     return userData; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteStudent(username: string): Promise<StudentDTO | null> { | export async function deleteStudent(username: string): Promise<StudentDTO> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
| 
 | 
 | ||||||
|     const user = await studentRepository.findByUsername(username); |     const student = await fetchStudent(username); // Throws error if it does not exist
 | ||||||
| 
 | 
 | ||||||
|     if (!user) { |     await studentRepository.deleteByUsername(username); | ||||||
|         return null; |     return mapToStudentDTO(student); | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
|         await studentRepository.deleteByUsername(username); |  | ||||||
| 
 |  | ||||||
|         return mapToStudentDTO(user); |  | ||||||
|     } catch (e) { |  | ||||||
|         getLogger().error(e); |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentClasses(username: string, full: boolean): Promise<ClassDTO[] | string[]> { | export async function getStudentClasses(username: string, full: boolean): Promise<ClassDTO[] | string[]> { | ||||||
|     const studentRepository = getStudentRepository(); |     const student = await fetchStudent(username); | ||||||
|     const student = await studentRepository.findByUsername(username); |  | ||||||
| 
 |  | ||||||
|     if (!student) { |  | ||||||
|         return []; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|     const classes = await classRepository.findByStudent(student); |     const classes = await classRepository.findByStudent(student); | ||||||
|  | @ -74,12 +82,7 @@ export async function getStudentClasses(username: string, full: boolean): Promis | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentAssignments(username: string, full: boolean): Promise<AssignmentDTO[]> { | export async function getStudentAssignments(username: string, full: boolean): Promise<AssignmentDTO[]> { | ||||||
|     const studentRepository = getStudentRepository(); |     const student = await fetchStudent(username); | ||||||
|     const student = await studentRepository.findByUsername(username); |  | ||||||
| 
 |  | ||||||
|     if (!student) { |  | ||||||
|         return []; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|     const classes = await classRepository.findByStudent(student); |     const classes = await classRepository.findByStudent(student); | ||||||
|  | @ -88,12 +91,7 @@ export async function getStudentAssignments(username: string, full: boolean): Pr | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[]> { | export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[]> { | ||||||
|     const studentRepository = getStudentRepository(); |     const student = await fetchStudent(username); | ||||||
|     const student = await studentRepository.findByUsername(username); |  | ||||||
| 
 |  | ||||||
|     if (!student) { |  | ||||||
|         return []; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const groupRepository = getGroupRepository(); |     const groupRepository = getGroupRepository(); | ||||||
|     const groups = await groupRepository.findAllGroupsWithStudent(student); |     const groups = await groupRepository.findAllGroupsWithStudent(student); | ||||||
|  | @ -106,12 +104,7 @@ export async function getStudentGroups(username: string, full: boolean): Promise | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> { | export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> { | ||||||
|     const studentRepository = getStudentRepository(); |     const student = await fetchStudent(username); | ||||||
|     const student = await studentRepository.findByUsername(username); |  | ||||||
| 
 |  | ||||||
|     if (!student) { |  | ||||||
|         return []; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const submissionRepository = getSubmissionRepository(); |     const submissionRepository = getSubmissionRepository(); | ||||||
|     const submissions = await submissionRepository.findAllSubmissionsForStudent(student); |     const submissions = await submissionRepository.findAllSubmissionsForStudent(student); | ||||||
|  | @ -122,3 +115,66 @@ export async function getStudentSubmissions(username: string, full: boolean): Pr | ||||||
| 
 | 
 | ||||||
|     return submissions.map(mapToSubmissionDTOId); |     return submissions.map(mapToSubmissionDTOId); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export async function getStudentQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> { | ||||||
|  |     const student = await fetchStudent(username); | ||||||
|  | 
 | ||||||
|  |     const questionRepository = getQuestionRepository(); | ||||||
|  |     const questions = await questionRepository.findAllByAuthor(student); | ||||||
|  | 
 | ||||||
|  |     if (full) { | ||||||
|  |         return questions.map(mapToQuestionDTO); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return questions.map(mapToQuestionDTOId); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createClassJoinRequest(username: string, classId: string): Promise<ClassJoinRequestDTO> { | ||||||
|  |     const requestRepo = getClassJoinRequestRepository(); | ||||||
|  | 
 | ||||||
|  |     const student = await fetchStudent(username); // Throws error if student not found
 | ||||||
|  |     const cls = await fetchClass(classId); | ||||||
|  | 
 | ||||||
|  |     const request = mapToStudentRequest(student, cls); | ||||||
|  |     await requestRepo.save(request, { preventOverwrite: true }); | ||||||
|  |     return mapToStudentRequestDTO(request); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getJoinRequestsByStudent(username: string): Promise<ClassJoinRequestDTO[]> { | ||||||
|  |     const requestRepo = getClassJoinRequestRepository(); | ||||||
|  | 
 | ||||||
|  |     const student = await fetchStudent(username); | ||||||
|  | 
 | ||||||
|  |     const requests = await requestRepo.findAllRequestsBy(student); | ||||||
|  |     return requests.map(mapToStudentRequestDTO); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getJoinRequestByStudentClass(username: string, classId: string): Promise<ClassJoinRequestDTO> { | ||||||
|  |     const requestRepo = getClassJoinRequestRepository(); | ||||||
|  | 
 | ||||||
|  |     const student = await fetchStudent(username); | ||||||
|  |     const cls = await fetchClass(classId); | ||||||
|  | 
 | ||||||
|  |     const request = await requestRepo.findByStudentAndClass(student, cls); | ||||||
|  |     if (!request) { | ||||||
|  |         throw new NotFoundException('Join request not found'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return mapToStudentRequestDTO(request); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteClassJoinRequest(username: string, classId: string): Promise<ClassJoinRequestDTO> { | ||||||
|  |     const requestRepo = getClassJoinRequestRepository(); | ||||||
|  | 
 | ||||||
|  |     const student = await fetchStudent(username); | ||||||
|  |     const cls = await fetchClass(classId); | ||||||
|  | 
 | ||||||
|  |     const request = await requestRepo.findByStudentAndClass(student, cls); | ||||||
|  | 
 | ||||||
|  |     if (!request) { | ||||||
|  |         throw new NotFoundException('Join request not found'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     await requestRepo.deleteBy(student, cls); | ||||||
|  |     return mapToStudentRequestDTO(request); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,137 +1,165 @@ | ||||||
| import { getClassRepository, getLearningObjectRepository, getQuestionRepository, getTeacherRepository } from '../data/repositories.js'; | import { | ||||||
|  |     getClassJoinRequestRepository, | ||||||
|  |     getClassRepository, | ||||||
|  |     getLearningObjectRepository, | ||||||
|  |     getQuestionRepository, | ||||||
|  |     getTeacherRepository, | ||||||
|  | } from '../data/repositories.js'; | ||||||
| import { mapToClassDTO } from '../interfaces/class.js'; | import { mapToClassDTO } from '../interfaces/class.js'; | ||||||
| import { getClassStudents } from './classes.js'; | import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; | ||||||
| import { mapToQuestionDTO, mapToQuestionId } from '../interfaces/question.js'; |  | ||||||
| import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js'; | import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js'; | ||||||
| import { ClassDTO } from '@dwengo-1/common/interfaces/class'; | import { Teacher } from '../entities/users/teacher.entity.js'; | ||||||
|  | import { fetchStudent } from './students.js'; | ||||||
|  | import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js'; | ||||||
|  | import { mapToStudentRequestDTO } from '../interfaces/student-request.js'; | ||||||
|  | import { TeacherRepository } from '../data/users/teacher-repository.js'; | ||||||
|  | import { ClassRepository } from '../data/classes/class-repository.js'; | ||||||
|  | import { Class } from '../entities/classes/class.entity.js'; | ||||||
|  | import { LearningObjectRepository } from '../data/content/learning-object-repository.js'; | ||||||
|  | import { LearningObject } from '../entities/content/learning-object.entity.js'; | ||||||
|  | import { QuestionRepository } from '../data/questions/question-repository.js'; | ||||||
|  | import { Question } from '../entities/questions/question.entity.js'; | ||||||
|  | import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js'; | ||||||
|  | import { Student } from '../entities/users/student.entity.js'; | ||||||
|  | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
|  | import { getClassStudents } from './classes.js'; | ||||||
| import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | ||||||
|  | import { ClassDTO } from '@dwengo-1/common/interfaces/class'; | ||||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||||
| import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||||
| import { getLogger } from '../logging/initalize.js'; | import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; | ||||||
|  | import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; | ||||||
| 
 | 
 | ||||||
| export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> { | export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||||
|     const teachers = await teacherRepository.findAll(); |     const users: Teacher[] = await teacherRepository.findAll(); | ||||||
| 
 | 
 | ||||||
|     if (full) { |     if (full) { | ||||||
|         return teachers.map(mapToTeacherDTO); |         return users.map(mapToTeacherDTO); | ||||||
|  |     } | ||||||
|  |     return users.map((user) => user.username); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function fetchTeacher(username: string): Promise<Teacher> { | ||||||
|  |     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||||
|  |     const user: Teacher | null = await teacherRepository.findByUsername(username); | ||||||
|  | 
 | ||||||
|  |     if (!user) { | ||||||
|  |         throw new NotFoundException('Teacher with username not found'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return teachers.map((teacher) => teacher.username); |     return user; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacher(username: string): Promise<TeacherDTO | null> { | export async function getTeacher(username: string): Promise<TeacherDTO> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const user: Teacher = await fetchTeacher(username); | ||||||
|     const user = await teacherRepository.findByUsername(username); |     return mapToTeacherDTO(user); | ||||||
|     return user ? mapToTeacherDTO(user) : null; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> { | export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||||
| 
 | 
 | ||||||
|     const newTeacher = mapToTeacher(userData); |     const newTeacher = mapToTeacher(userData); | ||||||
|     await teacherRepository.save(newTeacher, { preventOverwrite: true }); |     await teacherRepository.save(newTeacher, { preventOverwrite: true }); | ||||||
| 
 |  | ||||||
|     return mapToTeacherDTO(newTeacher); |     return mapToTeacherDTO(newTeacher); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteTeacher(username: string): Promise<TeacherDTO | null> { | export async function deleteTeacher(username: string): Promise<TeacherDTO> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||||
| 
 | 
 | ||||||
|     const user = await teacherRepository.findByUsername(username); |     const teacher = await fetchTeacher(username); // Throws error if it does not exist
 | ||||||
| 
 | 
 | ||||||
|     if (!user) { |     await teacherRepository.deleteByUsername(username); | ||||||
|         return null; |     return mapToTeacherDTO(teacher); | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
|         await teacherRepository.deleteByUsername(username); |  | ||||||
| 
 |  | ||||||
|         return mapToTeacherDTO(user); |  | ||||||
|     } catch (e) { |  | ||||||
|         getLogger().error(e); |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[] | null> { | async function fetchClassesByTeacher(username: string): Promise<ClassDTO[]> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacher: Teacher = await fetchTeacher(username); | ||||||
|     const teacher = await teacherRepository.findByUsername(username); |  | ||||||
|     if (!teacher) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const classRepository = getClassRepository(); |     const classRepository: ClassRepository = getClassRepository(); | ||||||
|     const classes = await classRepository.findByTeacher(teacher); |     const classes: Class[] = await classRepository.findByTeacher(teacher); | ||||||
|     return classes.map(mapToClassDTO); |     return classes.map(mapToClassDTO); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getClassesByTeacher(username: string, full: boolean): Promise<ClassDTO[] | string[] | null> { | export async function getClassesByTeacher(username: string, full: boolean): Promise<ClassDTO[] | string[]> { | ||||||
|     const classes = await fetchClassesByTeacher(username); |     const classes: ClassDTO[] = await fetchClassesByTeacher(username); | ||||||
| 
 |  | ||||||
|     if (!classes) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     if (full) { |     if (full) { | ||||||
|         return classes; |         return classes; | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     return classes.map((cls) => cls.id); |     return classes.map((cls) => cls.id); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function fetchStudentsByTeacher(username: string): Promise<StudentDTO[] | null> { | export async function getStudentsByTeacher(username: string, full: boolean): Promise<StudentDTO[] | string[]> { | ||||||
|     const classes = (await getClassesByTeacher(username, false)) as string[]; |     const classes: ClassDTO[] = await fetchClassesByTeacher(username); | ||||||
| 
 | 
 | ||||||
|     if (!classes) { |     if (!classes || classes.length === 0) { | ||||||
|         return null; |         return []; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat(); |     const classIds: string[] = classes.map((cls) => cls.id); | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getStudentsByTeacher(username: string, full: boolean): Promise<StudentDTO[] | string[] | null> { |  | ||||||
|     const students = await fetchStudentsByTeacher(username); |  | ||||||
| 
 |  | ||||||
|     if (!students) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|  |     const students: StudentDTO[] = (await Promise.all(classIds.map(async (id) => getClassStudents(id)))).flat(); | ||||||
|     if (full) { |     if (full) { | ||||||
|         return students; |         return students; | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     return students.map((student) => student.username); |     return students.map((student) => student.username); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[] | null> { | export async function getTeacherQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacher: Teacher = await fetchTeacher(username); | ||||||
|     const teacher = await teacherRepository.findByUsername(username); |  | ||||||
|     if (!teacher) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     // Find all learning objects that this teacher manages
 |     // Find all learning objects that this teacher manages
 | ||||||
|     const learningObjectRepository = getLearningObjectRepository(); |     const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository(); | ||||||
|     const learningObjects = await learningObjectRepository.findAllByTeacher(teacher); |     const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher); | ||||||
|  | 
 | ||||||
|  |     if (!learningObjects || learningObjects.length === 0) { | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     // Fetch all questions related to these learning objects
 |     // Fetch all questions related to these learning objects
 | ||||||
|     const questionRepository = getQuestionRepository(); |     const questionRepository: QuestionRepository = getQuestionRepository(); | ||||||
|     const questions = await questionRepository.findAllByLearningObjects(learningObjects); |     const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects); | ||||||
| 
 |  | ||||||
|     return questions.map(mapToQuestionDTO); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getQuestionsByTeacher(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[] | null> { |  | ||||||
|     const questions = await fetchTeacherQuestions(username); |  | ||||||
| 
 |  | ||||||
|     if (!questions) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     if (full) { |     if (full) { | ||||||
|         return questions; |         return questions.map(mapToQuestionDTO); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return questions.map(mapToQuestionId); |     return questions.map(mapToQuestionDTOId); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> { | ||||||
|  |     const classRepository: ClassRepository = getClassRepository(); | ||||||
|  |     const cls: Class | null = await classRepository.findById(classId); | ||||||
|  | 
 | ||||||
|  |     if (!cls) { | ||||||
|  |         throw new NotFoundException('Class with id not found'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository(); | ||||||
|  |     const requests: ClassJoinRequest[] = await requestRepo.findAllOpenRequestsTo(cls); | ||||||
|  |     return requests.map(mapToStudentRequestDTO); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function updateClassJoinRequestStatus(studentUsername: string, classId: string, accepted = true): Promise<ClassJoinRequestDTO> { | ||||||
|  |     const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository(); | ||||||
|  |     const classRepo: ClassRepository = getClassRepository(); | ||||||
|  | 
 | ||||||
|  |     const student: Student = await fetchStudent(studentUsername); | ||||||
|  |     const cls: Class | null = await classRepo.findById(classId); | ||||||
|  | 
 | ||||||
|  |     if (!cls) { | ||||||
|  |         throw new NotFoundException('Class not found'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const request: ClassJoinRequest | null = await requestRepo.findByStudentAndClass(student, cls); | ||||||
|  | 
 | ||||||
|  |     if (!request) { | ||||||
|  |         throw new NotFoundException('Join request not found'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     request.status = accepted ? ClassJoinRequestStatus.Accepted : ClassJoinRequestStatus.Declined; | ||||||
|  | 
 | ||||||
|  |     await requestRepo.save(request); | ||||||
|  |     return mapToStudentRequestDTO(request); | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										232
									
								
								backend/tests/controllers/students.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								backend/tests/controllers/students.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,232 @@ | ||||||
|  | import { setupTestApp } from '../setup-tests.js'; | ||||||
|  | import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest'; | ||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import { | ||||||
|  |     getAllStudentsHandler, | ||||||
|  |     getStudentHandler, | ||||||
|  |     createStudentHandler, | ||||||
|  |     deleteStudentHandler, | ||||||
|  |     getStudentClassesHandler, | ||||||
|  |     getStudentGroupsHandler, | ||||||
|  |     getStudentSubmissionsHandler, | ||||||
|  |     getStudentQuestionsHandler, | ||||||
|  |     createStudentRequestHandler, | ||||||
|  |     getStudentRequestsHandler, | ||||||
|  |     deleteClassJoinRequestHandler, | ||||||
|  |     getStudentRequestHandler, | ||||||
|  | } from '../../src/controllers/students.js'; | ||||||
|  | import { TEST_STUDENTS } from '../test_assets/users/students.testdata.js'; | ||||||
|  | import { NotFoundException } from '../../src/exceptions/not-found-exception.js'; | ||||||
|  | import { BadRequestException } from '../../src/exceptions/bad-request-exception.js'; | ||||||
|  | import { ConflictException } from '../../src/exceptions/conflict-exception.js'; | ||||||
|  | import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; | ||||||
|  | import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||||
|  | 
 | ||||||
|  | describe('Student controllers', () => { | ||||||
|  |     let req: Partial<Request>; | ||||||
|  |     let res: Partial<Response>; | ||||||
|  | 
 | ||||||
|  |     let jsonMock: Mock; | ||||||
|  | 
 | ||||||
|  |     beforeAll(async () => { | ||||||
|  |         await setupTestApp(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     beforeEach(() => { | ||||||
|  |         jsonMock = vi.fn(); | ||||||
|  |         res = { | ||||||
|  |             json: jsonMock, | ||||||
|  |         }; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get student', async () => { | ||||||
|  |         req = { params: { username: 'DireStraits' } }; | ||||||
|  | 
 | ||||||
|  |         await getStudentHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ student: expect.anything() })); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Student not found', async () => { | ||||||
|  |         req = { params: { username: 'doesnotexist' } }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => getStudentHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('No username', async () => { | ||||||
|  |         req = { params: {} }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => getStudentHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Create and delete student', async () => { | ||||||
|  |         const student = { | ||||||
|  |             id: 'coolstudent', | ||||||
|  |             username: 'coolstudent', | ||||||
|  |             firstName: 'New', | ||||||
|  |             lastName: 'Student', | ||||||
|  |         } as StudentDTO; | ||||||
|  |         req = { | ||||||
|  |             body: student, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await createStudentHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ student: expect.objectContaining(student) })); | ||||||
|  | 
 | ||||||
|  |         req = { params: { username: 'coolstudent' } }; | ||||||
|  | 
 | ||||||
|  |         await deleteStudentHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ student: expect.objectContaining(student) })); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Create duplicate student', async () => { | ||||||
|  |         req = { | ||||||
|  |             body: { | ||||||
|  |                 username: 'DireStraits', | ||||||
|  |                 firstName: 'dupe', | ||||||
|  |                 lastName: 'dupe', | ||||||
|  |             }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => createStudentHandler(req as Request, res as Response)).rejects.toThrowError(EntityAlreadyExistsException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Create student no body', async () => { | ||||||
|  |         req = { body: {} }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => createStudentHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Student list', async () => { | ||||||
|  |         req = { query: { full: 'true' } }; | ||||||
|  | 
 | ||||||
|  |         await getAllStudentsHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ students: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  | 
 | ||||||
|  |         // Check is DireStraits is part of the student list
 | ||||||
|  |         const studentUsernames = result.students.map((s: StudentDTO) => s.username); | ||||||
|  |         expect(studentUsernames).toContain('DireStraits'); | ||||||
|  | 
 | ||||||
|  |         // Check length, +1 because of create
 | ||||||
|  |         expect(result.students).toHaveLength(TEST_STUDENTS.length); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Student classes', async () => { | ||||||
|  |         req = { params: { username: 'DireStraits' }, query: {} }; | ||||||
|  | 
 | ||||||
|  |         await getStudentClassesHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ classes: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         expect(result.classes).to.have.length.greaterThan(0); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Student groups', async () => { | ||||||
|  |         req = { params: { username: 'DireStraits' }, query: {} }; | ||||||
|  | 
 | ||||||
|  |         await getStudentGroupsHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ groups: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         expect(result.groups).to.have.length.greaterThan(0); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Student submissions', async () => { | ||||||
|  |         req = { params: { username: 'DireStraits' }, query: { full: 'true' } }; | ||||||
|  | 
 | ||||||
|  |         await getStudentSubmissionsHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         expect(result.submissions).to.have.length.greaterThan(0); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Student questions', async () => { | ||||||
|  |         req = { params: { username: 'DireStraits' }, query: { full: 'true' } }; | ||||||
|  | 
 | ||||||
|  |         await getStudentQuestionsHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ questions: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         expect(result.questions).to.have.length.greaterThan(0); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Deleting non-existent student', async () => { | ||||||
|  |         req = { params: { username: 'doesnotexist' } }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => deleteStudentHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get join requests by student', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { username: 'PinkFloyd' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getStudentRequestsHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith( | ||||||
|  |             expect.objectContaining({ | ||||||
|  |                 requests: expect.anything(), | ||||||
|  |             }) | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         // Console.log('[JOIN REQUESTS]', result.requests);
 | ||||||
|  |         expect(result.requests.length).toBeGreaterThan(0); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get join request by student and class', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { username: 'PinkFloyd', classId: 'id02' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getStudentRequestHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith( | ||||||
|  |             expect.objectContaining({ | ||||||
|  |                 request: expect.anything(), | ||||||
|  |             }) | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Create join request', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { username: 'Noordkaap' }, | ||||||
|  |             body: { classId: 'id02' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await createStudentRequestHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Create join request duplicate', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { username: 'Tool' }, | ||||||
|  |             body: { classId: 'id02' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Delete join request', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { username: 'Noordkaap', classId: 'id02' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await deleteClassJoinRequestHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         await expect(async () => deleteClassJoinRequestHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
							
								
								
									
										204
									
								
								backend/tests/controllers/teachers.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								backend/tests/controllers/teachers.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,204 @@ | ||||||
|  | import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; | ||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import { setupTestApp } from '../setup-tests.js'; | ||||||
|  | import { NotFoundException } from '../../src/exceptions/not-found-exception.js'; | ||||||
|  | import { | ||||||
|  |     createTeacherHandler, | ||||||
|  |     deleteTeacherHandler, | ||||||
|  |     getAllTeachersHandler, | ||||||
|  |     getStudentJoinRequestHandler, | ||||||
|  |     getTeacherClassHandler, | ||||||
|  |     getTeacherHandler, | ||||||
|  |     getTeacherStudentHandler, | ||||||
|  |     updateStudentJoinRequestHandler, | ||||||
|  | } from '../../src/controllers/teachers.js'; | ||||||
|  | import { BadRequestException } from '../../src/exceptions/bad-request-exception.js'; | ||||||
|  | import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; | ||||||
|  | import { getStudentRequestsHandler } from '../../src/controllers/students.js'; | ||||||
|  | import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | ||||||
|  | 
 | ||||||
|  | describe('Teacher controllers', () => { | ||||||
|  |     let req: Partial<Request>; | ||||||
|  |     let res: Partial<Response>; | ||||||
|  | 
 | ||||||
|  |     let jsonMock: Mock; | ||||||
|  | 
 | ||||||
|  |     beforeAll(async () => { | ||||||
|  |         await setupTestApp(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     beforeEach(() => { | ||||||
|  |         jsonMock = vi.fn(); | ||||||
|  |         res = { | ||||||
|  |             json: jsonMock, | ||||||
|  |         }; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get teacher', async () => { | ||||||
|  |         req = { params: { username: 'FooFighters' } }; | ||||||
|  | 
 | ||||||
|  |         await getTeacherHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ teacher: expect.anything() })); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Teacher not found', async () => { | ||||||
|  |         req = { params: { username: 'doesnotexist' } }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => getTeacherHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('No username', async () => { | ||||||
|  |         req = { params: {} }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => getTeacherHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Create and delete teacher', async () => { | ||||||
|  |         const teacher = { | ||||||
|  |             id: 'coolteacher', | ||||||
|  |             username: 'coolteacher', | ||||||
|  |             firstName: 'New', | ||||||
|  |             lastName: 'Teacher', | ||||||
|  |         }; | ||||||
|  |         req = { | ||||||
|  |             body: teacher, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await createTeacherHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ teacher: expect.objectContaining(teacher) })); | ||||||
|  | 
 | ||||||
|  |         req = { params: { username: 'coolteacher' } }; | ||||||
|  | 
 | ||||||
|  |         await deleteTeacherHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ teacher: expect.objectContaining(teacher) })); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Create duplicate student', async () => { | ||||||
|  |         req = { | ||||||
|  |             body: { | ||||||
|  |                 username: 'FooFighters', | ||||||
|  |                 firstName: 'Dave', | ||||||
|  |                 lastName: 'Grohl', | ||||||
|  |             }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => createTeacherHandler(req as Request, res as Response)).rejects.toThrowError(EntityAlreadyExistsException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Create teacher no body', async () => { | ||||||
|  |         req = { body: {} }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => createTeacherHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Teacher list', async () => { | ||||||
|  |         req = { query: { full: 'true' } }; | ||||||
|  | 
 | ||||||
|  |         await getAllTeachersHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ teachers: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  | 
 | ||||||
|  |         const teacherUsernames = result.teachers.map((s: TeacherDTO) => s.username); | ||||||
|  |         expect(teacherUsernames).toContain('FooFighters'); | ||||||
|  | 
 | ||||||
|  |         expect(result.teachers).toHaveLength(4); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Deleting non-existent student', async () => { | ||||||
|  |         req = { params: { username: 'doesnotexist' } }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => deleteTeacherHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get teacher classes', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { username: 'FooFighters' }, | ||||||
|  |             query: { full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getTeacherClassHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ classes: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         // Console.log('[TEACHER CLASSES]', result);
 | ||||||
|  |         expect(result.classes.length).toBeGreaterThan(0); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get teacher students', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { username: 'FooFighters' }, | ||||||
|  |             query: { full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getTeacherStudentHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ students: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         // Console.log('[TEACHER STUDENTS]', result.students);
 | ||||||
|  |         expect(result.students.length).toBeGreaterThan(0); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  | 
 | ||||||
|  |     It('Get teacher questions', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { username: 'FooFighters' }, | ||||||
|  |             query: { full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getTeacherQuestionHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ questions: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         // console.log('[TEACHER QUESTIONS]', result.questions);
 | ||||||
|  |         expect(result.questions.length).toBeGreaterThan(0); | ||||||
|  | 
 | ||||||
|  |         // TODO fix
 | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |      */ | ||||||
|  | 
 | ||||||
|  |     it('Get join requests by class', async () => { | ||||||
|  |         req = { | ||||||
|  |             query: { username: 'LimpBizkit' }, | ||||||
|  |             params: { classId: 'id02' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getStudentJoinRequestHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ joinRequests: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         // Console.log('[JOIN REQUESTS FOR CLASS]', result.joinRequests);
 | ||||||
|  |         expect(result.joinRequests.length).toBeGreaterThan(0); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Update join request status', async () => { | ||||||
|  |         req = { | ||||||
|  |             query: { username: 'LimpBizkit', studentUsername: 'PinkFloyd' }, | ||||||
|  |             params: { classId: 'id02' }, | ||||||
|  |             body: { accepted: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await updateStudentJoinRequestHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         req = { | ||||||
|  |             params: { username: 'PinkFloyd' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getStudentRequestsHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         const status: boolean = jsonMock.mock.lastCall?.[0].requests[0].status; | ||||||
|  |         expect(status).toBeTruthy(); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | @ -1,49 +1,19 @@ | ||||||
| import { EntityManager } from '@mikro-orm/core'; | import { EntityManager } from '@mikro-orm/core'; | ||||||
| import { Student } from '../../../src/entities/users/student.entity'; | import { Student } from '../../../src/entities/users/student.entity'; | ||||||
| 
 | 
 | ||||||
|  | // 🔓 Ruwe testdata array — herbruikbaar in assertions
 | ||||||
|  | export const TEST_STUDENTS = [ | ||||||
|  |     { username: 'Noordkaap', firstName: 'Stijn', lastName: 'Meuris' }, | ||||||
|  |     { username: 'DireStraits', firstName: 'Mark', lastName: 'Knopfler' }, | ||||||
|  |     { username: 'Tool', firstName: 'Maynard', lastName: 'Keenan' }, | ||||||
|  |     { username: 'SmashingPumpkins', firstName: 'Billy', lastName: 'Corgan' }, | ||||||
|  |     { username: 'PinkFloyd', firstName: 'David', lastName: 'Gilmoure' }, | ||||||
|  |     { username: 'TheDoors', firstName: 'Jim', lastName: 'Morisson' }, | ||||||
|  |     // ⚠️ Deze mag niet gebruikt worden in elke test!
 | ||||||
|  |     { username: 'Nirvana', firstName: 'Kurt', lastName: 'Cobain' }, | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | // 🏗️ Functie die ORM entities maakt uit de data array
 | ||||||
| export function makeTestStudents(em: EntityManager): Student[] { | export function makeTestStudents(em: EntityManager): Student[] { | ||||||
|     const student01 = em.create(Student, { |     return TEST_STUDENTS.map((data) => em.create(Student, data)); | ||||||
|         username: 'Noordkaap', |  | ||||||
|         firstName: 'Stijn', |  | ||||||
|         lastName: 'Meuris', |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const student02 = em.create(Student, { |  | ||||||
|         username: 'DireStraits', |  | ||||||
|         firstName: 'Mark', |  | ||||||
|         lastName: 'Knopfler', |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const student03 = em.create(Student, { |  | ||||||
|         username: 'Tool', |  | ||||||
|         firstName: 'Maynard', |  | ||||||
|         lastName: 'Keenan', |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const student04 = em.create(Student, { |  | ||||||
|         username: 'SmashingPumpkins', |  | ||||||
|         firstName: 'Billy', |  | ||||||
|         lastName: 'Corgan', |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const student05 = em.create(Student, { |  | ||||||
|         username: 'PinkFloyd', |  | ||||||
|         firstName: 'David', |  | ||||||
|         lastName: 'Gilmoure', |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const student06 = em.create(Student, { |  | ||||||
|         username: 'TheDoors', |  | ||||||
|         firstName: 'Jim', |  | ||||||
|         lastName: 'Morisson', |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // Do not use for any tests, gets deleted in a unit test
 |  | ||||||
|     const student07 = em.create(Student, { |  | ||||||
|         username: 'Nirvana', |  | ||||||
|         firstName: 'Kurt', |  | ||||||
|         lastName: 'Cobain', |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return [student01, student02, student03, student04, student05, student06, student07]; |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										8
									
								
								common/src/interfaces/class-join-request.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								common/src/interfaces/class-join-request.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | import { StudentDTO } from './student'; | ||||||
|  | import { ClassJoinRequestStatus } from '../util/class-join-request'; | ||||||
|  | 
 | ||||||
|  | export interface ClassJoinRequestDTO { | ||||||
|  |     requester: StudentDTO; | ||||||
|  |     class: string; | ||||||
|  |     status: ClassJoinRequestStatus; | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								common/src/interfaces/theme.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								common/src/interfaces/theme.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | export interface Theme { | ||||||
|  |     title: string; | ||||||
|  |     hruids: string[]; | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								frontend/src/controllers/assignments.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/controllers/assignments.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | import type { AssignmentDTO } from "@dwengo-1/interfaces/assignment"; | ||||||
|  | 
 | ||||||
|  | export interface AssignmentsResponse { | ||||||
|  |     assignments: AssignmentDTO[]; | ||||||
|  | } // TODO ID
 | ||||||
							
								
								
									
										5
									
								
								frontend/src/controllers/classes.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/controllers/classes.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | import type { ClassDTO } from "@dwengo-1/interfaces/class"; | ||||||
|  | 
 | ||||||
|  | export interface ClassesResponse { | ||||||
|  |     classes: ClassDTO[] | string[]; | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								frontend/src/controllers/groups.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/controllers/groups.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | import type { GroupDTO } from "@dwengo-1/interfaces/group"; | ||||||
|  | 
 | ||||||
|  | export interface GroupsResponse { | ||||||
|  |     groups: GroupDTO[]; | ||||||
|  | } // | TODO id
 | ||||||
							
								
								
									
										5
									
								
								frontend/src/controllers/questions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/controllers/questions.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | import type { QuestionDTO, QuestionId } from "@dwengo-1/interfaces/question"; | ||||||
|  | 
 | ||||||
|  | export interface QuestionsResponse { | ||||||
|  |     questions: QuestionDTO[] | QuestionId[]; | ||||||
|  | } | ||||||
							
								
								
									
										79
									
								
								frontend/src/controllers/students.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								frontend/src/controllers/students.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | ||||||
|  | import { BaseController } from "@/controllers/base-controller.ts"; | ||||||
|  | import type { ClassesResponse } from "@/controllers/classes.ts"; | ||||||
|  | import type { AssignmentsResponse } from "@/controllers/assignments.ts"; | ||||||
|  | import type { GroupsResponse } from "@/controllers/groups.ts"; | ||||||
|  | import type { SubmissionsResponse } from "@/controllers/submissions.ts"; | ||||||
|  | import type { QuestionsResponse } from "@/controllers/questions.ts"; | ||||||
|  | import type { StudentDTO } from "@dwengo-1/interfaces/student"; | ||||||
|  | import type { ClassJoinRequestDTO } from "@dwengo-1/interfaces/class-join-request"; | ||||||
|  | 
 | ||||||
|  | export interface StudentsResponse { | ||||||
|  |     students: StudentDTO[] | string[]; | ||||||
|  | } | ||||||
|  | export interface StudentResponse { | ||||||
|  |     student: StudentDTO; | ||||||
|  | } | ||||||
|  | export interface JoinRequestsResponse { | ||||||
|  |     requests: ClassJoinRequestDTO[]; | ||||||
|  | } | ||||||
|  | export interface JoinRequestResponse { | ||||||
|  |     request: ClassJoinRequestDTO; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class StudentController extends BaseController { | ||||||
|  |     constructor() { | ||||||
|  |         super("student"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getAll(full = true): Promise<StudentsResponse> { | ||||||
|  |         return this.get<StudentsResponse>("/", { full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getByUsername(username: string): Promise<StudentResponse> { | ||||||
|  |         return this.get<StudentResponse>(`/${username}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async createStudent(data: StudentDTO): Promise<StudentResponse> { | ||||||
|  |         return this.post<StudentResponse>("/", data); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async deleteStudent(username: string): Promise<StudentResponse> { | ||||||
|  |         return this.delete<StudentResponse>(`/${username}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getClasses(username: string, full = true): Promise<ClassesResponse> { | ||||||
|  |         return this.get<ClassesResponse>(`/${username}/classes`, { full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getAssignments(username: string, full = true): Promise<AssignmentsResponse> { | ||||||
|  |         return this.get<AssignmentsResponse>(`/${username}/assignments`, { full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getGroups(username: string, full = true): Promise<GroupsResponse> { | ||||||
|  |         return this.get<GroupsResponse>(`/${username}/groups`, { full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getSubmissions(username: string): Promise<SubmissionsResponse> { | ||||||
|  |         return this.get<SubmissionsResponse>(`/${username}/submissions`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getQuestions(username: string, full = true): Promise<QuestionsResponse> { | ||||||
|  |         return this.get<QuestionsResponse>(`/${username}/questions`, { full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getJoinRequests(username: string): Promise<JoinRequestsResponse> { | ||||||
|  |         return this.get<JoinRequestsResponse>(`/${username}/joinRequests`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> { | ||||||
|  |         return this.get<JoinRequestResponse>(`/${username}/joinRequests/${classId}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async createJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> { | ||||||
|  |         return this.post<JoinRequestResponse>(`/${username}/joinRequests}`, classId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async deleteJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> { | ||||||
|  |         return this.delete<JoinRequestResponse>(`/${username}/joinRequests/${classId}`); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								frontend/src/controllers/submissions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/controllers/submissions.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | import { type SubmissionDTO, SubmissionDTOId } from "@dwengo-1/interfaces/submission"; | ||||||
|  | 
 | ||||||
|  | export interface SubmissionsResponse { | ||||||
|  |     submissions: SubmissionDTO[] | SubmissionDTOId[]; | ||||||
|  | } | ||||||
							
								
								
									
										64
									
								
								frontend/src/controllers/teachers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								frontend/src/controllers/teachers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | ||||||
|  | import { BaseController } from "@/controllers/base-controller.ts"; | ||||||
|  | import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts"; | ||||||
|  | import type { QuestionsResponse } from "@/controllers/questions.ts"; | ||||||
|  | import type { ClassesResponse } from "@/controllers/classes.ts"; | ||||||
|  | import type { TeacherDTO } from "@dwengo-1/interfaces/teacher"; | ||||||
|  | 
 | ||||||
|  | export interface TeachersResponse { | ||||||
|  |     teachers: TeacherDTO[] | string[]; | ||||||
|  | } | ||||||
|  | export interface TeacherResponse { | ||||||
|  |     teacher: TeacherDTO; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class TeacherController extends BaseController { | ||||||
|  |     constructor() { | ||||||
|  |         super("teacher"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getAll(full = false): Promise<TeachersResponse> { | ||||||
|  |         return this.get<TeachersResponse>("/", { full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getByUsername(username: string): Promise<TeacherResponse> { | ||||||
|  |         return this.get<TeacherResponse>(`/${username}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async createTeacher(data: TeacherDTO): Promise<TeacherResponse> { | ||||||
|  |         return this.post<TeacherResponse>("/", data); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async deleteTeacher(username: string): Promise<TeacherResponse> { | ||||||
|  |         return this.delete<TeacherResponse>(`/${username}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getClasses(username: string, full = false): Promise<ClassesResponse> { | ||||||
|  |         return this.get<ClassesResponse>(`/${username}/classes`, { full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getStudents(username: string, full = false): Promise<StudentsResponse> { | ||||||
|  |         return this.get<StudentsResponse>(`/${username}/students`, { full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getQuestions(username: string, full = false): Promise<QuestionsResponse> { | ||||||
|  |         return this.get<QuestionsResponse>(`/${username}/questions`, { full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getStudentJoinRequests(username: string, classId: string): Promise<JoinRequestsResponse> { | ||||||
|  |         return this.get<JoinRequestsResponse>(`/${username}/joinRequests/${classId}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async updateStudentJoinRequest( | ||||||
|  |         teacherUsername: string, | ||||||
|  |         classId: string, | ||||||
|  |         studentUsername: string, | ||||||
|  |         accepted: boolean, | ||||||
|  |     ): Promise<JoinRequestResponse> { | ||||||
|  |         return this.put<JoinRequestResponse>( | ||||||
|  |             `/${teacherUsername}/joinRequests/${classId}/${studentUsername}`, | ||||||
|  |             accepted, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // GetInvitations(id: string) {return this.get<{ invitations: string[] }>(`/${id}/invitations`);}
 | ||||||
|  | } | ||||||
|  | @ -1,11 +1,12 @@ | ||||||
| import { BaseController } from "@/controllers/base-controller.ts"; | import { BaseController } from "@/controllers/base-controller.ts"; | ||||||
|  | import type { Theme } from "@dwengo-1/interfaces/theme"; | ||||||
| 
 | 
 | ||||||
| export class ThemeController extends BaseController { | export class ThemeController extends BaseController { | ||||||
|     constructor() { |     constructor() { | ||||||
|         super("theme"); |         super("theme"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getAll(language: string | null = null): Promise<unknown> { |     async getAll(language: string | null = null): Promise<Theme[]> { | ||||||
|         const query = language ? { language } : undefined; |         const query = language ? { language } : undefined; | ||||||
|         return this.get("/", query); |         return this.get("/", query); | ||||||
|     } |     } | ||||||
|  |  | ||||||
							
								
								
									
										205
									
								
								frontend/src/queries/students.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								frontend/src/queries/students.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,205 @@ | ||||||
|  | import { computed, toValue } from "vue"; | ||||||
|  | import type { MaybeRefOrGetter } from "vue"; | ||||||
|  | import { | ||||||
|  |     useMutation, | ||||||
|  |     type UseMutationReturnType, | ||||||
|  |     useQuery, | ||||||
|  |     useQueryClient, | ||||||
|  |     type UseQueryReturnType, | ||||||
|  | } from "@tanstack/vue-query"; | ||||||
|  | import { | ||||||
|  |     type JoinRequestResponse, | ||||||
|  |     type JoinRequestsResponse, | ||||||
|  |     StudentController, | ||||||
|  |     type StudentResponse, | ||||||
|  |     type StudentsResponse, | ||||||
|  | } from "@/controllers/students.ts"; | ||||||
|  | import type { ClassesResponse } from "@/controllers/classes.ts"; | ||||||
|  | import type { AssignmentsResponse } from "@/controllers/assignments.ts"; | ||||||
|  | import type { GroupsResponse } from "@/controllers/groups.ts"; | ||||||
|  | import type { SubmissionsResponse } from "@/controllers/submissions.ts"; | ||||||
|  | import type { QuestionsResponse } from "@/controllers/questions.ts"; | ||||||
|  | import type { StudentDTO } from "@dwengo-1/interfaces/student"; | ||||||
|  | 
 | ||||||
|  | const studentController = new StudentController(); | ||||||
|  | 
 | ||||||
|  | /** 🔑 Query keys */ | ||||||
|  | function studentsQueryKey(full: boolean): [string, boolean] { | ||||||
|  |     return ["students", full]; | ||||||
|  | } | ||||||
|  | function studentQueryKey(username: string): [string, string] { | ||||||
|  |     return ["student", username]; | ||||||
|  | } | ||||||
|  | function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||||
|  |     return ["student-classes", username, full]; | ||||||
|  | } | ||||||
|  | function studentAssignmentsQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||||
|  |     return ["student-assignments", username, full]; | ||||||
|  | } | ||||||
|  | function studentGroupsQueryKeys(username: string, full: boolean): [string, string, boolean] { | ||||||
|  |     return ["student-groups", username, full]; | ||||||
|  | } | ||||||
|  | function studentSubmissionsQueryKey(username: string): [string, string] { | ||||||
|  |     return ["student-submissions", username]; | ||||||
|  | } | ||||||
|  | function studentQuestionsQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||||
|  |     return ["student-questions", username, full]; | ||||||
|  | } | ||||||
|  | export function studentJoinRequestsQueryKey(username: string): [string, string] { | ||||||
|  |     return ["student-join-requests", username]; | ||||||
|  | } | ||||||
|  | export function studentJoinRequestQueryKey(username: string, classId: string): [string, string, string] { | ||||||
|  |     return ["student-join-request", username, classId]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useStudentsQuery(full: MaybeRefOrGetter<boolean> = true): UseQueryReturnType<StudentsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => studentsQueryKey(toValue(full))), | ||||||
|  |         queryFn: async () => studentController.getAll(toValue(full)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useStudentQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  | ): UseQueryReturnType<StudentResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => studentQueryKey(toValue(username)!)), | ||||||
|  |         queryFn: async () => studentController.getByUsername(toValue(username)!), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useStudentClassesQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = true, | ||||||
|  | ): UseQueryReturnType<ClassesResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => studentClassesQueryKey(toValue(username)!, toValue(full))), | ||||||
|  |         queryFn: async () => studentController.getClasses(toValue(username)!, toValue(full)), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useStudentAssignmentsQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = true, | ||||||
|  | ): UseQueryReturnType<AssignmentsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => studentAssignmentsQueryKey(toValue(username)!, toValue(full))), | ||||||
|  |         queryFn: async () => studentController.getAssignments(toValue(username)!, toValue(full)), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useStudentGroupsQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = true, | ||||||
|  | ): UseQueryReturnType<GroupsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => studentGroupsQueryKeys(toValue(username)!, toValue(full))), | ||||||
|  |         queryFn: async () => studentController.getGroups(toValue(username)!, toValue(full)), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useStudentSubmissionsQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  | ): UseQueryReturnType<SubmissionsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => studentSubmissionsQueryKey(toValue(username)!)), | ||||||
|  |         queryFn: async () => studentController.getSubmissions(toValue(username)!), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useStudentQuestionsQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = true, | ||||||
|  | ): UseQueryReturnType<QuestionsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => studentQuestionsQueryKey(toValue(username)!, toValue(full))), | ||||||
|  |         queryFn: async () => studentController.getQuestions(toValue(username)!, toValue(full)), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useStudentJoinRequestsQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  | ): UseQueryReturnType<JoinRequestsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => studentJoinRequestsQueryKey(toValue(username)!)), | ||||||
|  |         queryFn: async () => studentController.getJoinRequests(toValue(username)!), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useStudentJoinRequestQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     classId: MaybeRefOrGetter<string | undefined>, | ||||||
|  | ): UseQueryReturnType<JoinRequestResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => studentJoinRequestQueryKey(toValue(username)!, toValue(classId)!)), | ||||||
|  |         queryFn: async () => studentController.getJoinRequest(toValue(username)!, toValue(classId)!), | ||||||
|  |         enabled: () => Boolean(toValue(username)) && Boolean(toValue(classId)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useCreateStudentMutation(): UseMutationReturnType<StudentResponse, Error, StudentDTO, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async (data) => studentController.createStudent(data), | ||||||
|  |         onSuccess: async () => { | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: ["students"] }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useDeleteStudentMutation(): UseMutationReturnType<StudentResponse, Error, string, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async (username) => studentController.deleteStudent(username), | ||||||
|  |         onSuccess: async (deletedUser) => { | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: ["students"] }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: studentQueryKey(deletedUser.student.username) }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useCreateJoinRequestMutation(): UseMutationReturnType< | ||||||
|  |     JoinRequestResponse, | ||||||
|  |     Error, | ||||||
|  |     { username: string; classId: string }, | ||||||
|  |     unknown | ||||||
|  | > { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId), | ||||||
|  |         onSuccess: async (newJoinRequest) => { | ||||||
|  |             await queryClient.invalidateQueries({ | ||||||
|  |                 queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester), | ||||||
|  |             }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useDeleteJoinRequestMutation(): UseMutationReturnType< | ||||||
|  |     JoinRequestResponse, | ||||||
|  |     Error, | ||||||
|  |     { username: string; classId: string }, | ||||||
|  |     unknown | ||||||
|  | > { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async ({ username, classId }) => studentController.deleteJoinRequest(username, classId), | ||||||
|  |         onSuccess: async (deletedJoinRequest) => { | ||||||
|  |             const username = deletedJoinRequest.request.requester; | ||||||
|  |             const classId = deletedJoinRequest.request.class; | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
							
								
								
									
										136
									
								
								frontend/src/queries/teachers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								frontend/src/queries/teachers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,136 @@ | ||||||
|  | import { computed, toValue } from "vue"; | ||||||
|  | import type { MaybeRefOrGetter } from "vue"; | ||||||
|  | import { useMutation, useQuery, useQueryClient, UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query"; | ||||||
|  | import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts"; | ||||||
|  | import type { ClassesResponse } from "@/controllers/classes.ts"; | ||||||
|  | import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts"; | ||||||
|  | import type { QuestionsResponse } from "@/controllers/questions.ts"; | ||||||
|  | import type { TeacherDTO } from "@dwengo-1/interfaces/teacher"; | ||||||
|  | import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts"; | ||||||
|  | 
 | ||||||
|  | const teacherController = new TeacherController(); | ||||||
|  | 
 | ||||||
|  | /** 🔑 Query keys */ | ||||||
|  | function teachersQueryKey(full: boolean): [string, boolean] { | ||||||
|  |     return ["teachers", full]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function teacherQueryKey(username: string): [string, string] { | ||||||
|  |     return ["teacher", username]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function teacherClassesQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||||
|  |     return ["teacher-classes", username, full]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function teacherStudentsQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||||
|  |     return ["teacher-students", username, full]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function teacherQuestionsQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||||
|  |     return ["teacher-questions", username, full]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useTeachersQuery(full: MaybeRefOrGetter<boolean> = false): UseQueryReturnType<TeachersResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => teachersQueryKey(toValue(full))), | ||||||
|  |         queryFn: async () => teacherController.getAll(toValue(full)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useTeacherQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  | ): UseQueryReturnType<TeacherResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => teacherQueryKey(toValue(username)!)), | ||||||
|  |         queryFn: async () => teacherController.getByUsername(toValue(username)!), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useTeacherClassesQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = false, | ||||||
|  | ): UseQueryReturnType<ClassesResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => teacherClassesQueryKey(toValue(username)!, toValue(full))), | ||||||
|  |         queryFn: async () => teacherController.getClasses(toValue(username)!, toValue(full)), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useTeacherStudentsQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = false, | ||||||
|  | ): UseQueryReturnType<StudentsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => teacherStudentsQueryKey(toValue(username)!, toValue(full))), | ||||||
|  |         queryFn: async () => teacherController.getStudents(toValue(username)!, toValue(full)), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useTeacherQuestionsQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = false, | ||||||
|  | ): UseQueryReturnType<QuestionsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => teacherQuestionsQueryKey(toValue(username)!, toValue(full))), | ||||||
|  |         queryFn: async () => teacherController.getQuestions(toValue(username)!, toValue(full)), | ||||||
|  |         enabled: () => Boolean(toValue(username)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useTeacherJoinRequestsQuery( | ||||||
|  |     username: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     classId: MaybeRefOrGetter<string | undefined>, | ||||||
|  | ): UseQueryReturnType<JoinRequestsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => JOIN_REQUESTS_QUERY_KEY(toValue(username)!, toValue(classId)!)), | ||||||
|  |         queryFn: async () => teacherController.getStudentJoinRequests(toValue(username)!, toValue(classId)!), | ||||||
|  |         enabled: () => Boolean(toValue(username)) && Boolean(toValue(classId)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useCreateTeacherMutation(): UseMutationReturnType<TeacherResponse, Error, TeacherDTO, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async (data: TeacherDTO) => teacherController.createTeacher(data), | ||||||
|  |         onSuccess: async () => { | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: ["teachers"] }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useDeleteTeacherMutation(): UseMutationReturnType<TeacherResponse, Error, string, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async (username: string) => teacherController.deleteTeacher(username), | ||||||
|  |         onSuccess: async (deletedTeacher) => { | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: ["teachers"] }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: teacherQueryKey(deletedTeacher.teacher.username) }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useUpdateJoinRequestMutation(): UseMutationReturnType< | ||||||
|  |     JoinRequestResponse, | ||||||
|  |     Error, | ||||||
|  |     { teacherUsername: string; classId: string; studentUsername: string; accepted: boolean }, | ||||||
|  |     unknown | ||||||
|  | > { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async ({ teacherUsername, classId, studentUsername, accepted }) => | ||||||
|  |             teacherController.updateStudentJoinRequest(teacherUsername, classId, studentUsername, accepted), | ||||||
|  |         onSuccess: async (deletedJoinRequest) => { | ||||||
|  |             const username = deletedJoinRequest.request.requester; | ||||||
|  |             const classId = deletedJoinRequest.request.class; | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; | import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||||
| import { getThemeController } from "@/controllers/controllers"; |  | ||||||
| import { type MaybeRefOrGetter, toValue } from "vue"; | import { type MaybeRefOrGetter, toValue } from "vue"; | ||||||
| import type { Theme } from "@/data-objects/theme.ts"; | import type { Theme } from "@dwengo-1/interfaces/theme"; | ||||||
|  | import { getThemeController } from "@/controllers/controllers.ts"; | ||||||
| 
 | 
 | ||||||
| const themeController = getThemeController(); | const themeController = getThemeController(); | ||||||
| 
 | 
 | ||||||
| export function useThemeQuery(language: MaybeRefOrGetter<string>): UseQueryReturnType<Theme[], Error> { | export function useThemeQuery(language: MaybeRefOrGetter<string | undefined>): UseQueryReturnType<Theme[], Error> { | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: ["themes", language], |         queryKey: ["themes", language], | ||||||
|         queryFn: async () => { |         queryFn: async () => { | ||||||
|  | @ -16,10 +16,12 @@ export function useThemeQuery(language: MaybeRefOrGetter<string>): UseQueryRetur | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useThemeHruidsQuery(themeKey: string | null): UseQueryReturnType<unknown, Error> { | export function useThemeHruidsQuery( | ||||||
|  |     themeKey: MaybeRefOrGetter<string | undefined>, | ||||||
|  | ): UseQueryReturnType<string[], Error> { | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: ["theme-hruids", themeKey], |         queryKey: ["theme-hruids", themeKey], | ||||||
|         queryFn: async () => themeController.getHruidsByKey(themeKey!), |         queryFn: async () => themeController.getHruidsByKey(toValue(themeKey)!), | ||||||
|         enabled: Boolean(themeKey), |         enabled: Boolean(themeKey), | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl