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 { | ||||
|     createClassJoinRequest, | ||||
|     createStudent, | ||||
|     deleteClassJoinRequest, | ||||
|     deleteStudent, | ||||
|     getAllStudents, | ||||
|     getJoinRequestByStudentClass, | ||||
|     getJoinRequestsByStudent, | ||||
|     getStudent, | ||||
|     getStudentAssignments, | ||||
|     getStudentClasses, | ||||
|     getStudentGroups, | ||||
|     getStudentQuestions, | ||||
|     getStudentSubmissions, | ||||
| } from '../services/students.js'; | ||||
| 
 | ||||
| import { requireFields } from './error-helper.js'; | ||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||
| 
 | ||||
| // TODO: accept arguments (full, ...)
 | ||||
| // TODO: endpoints
 | ||||
| export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     const students = await getAllStudents(full); | ||||
|     const students: StudentDTO[] | string[] = await getAllStudents(full); | ||||
| 
 | ||||
|     if (!students) { | ||||
|         res.status(404).json({ error: `Student not found.` }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json({ students: students }); | ||||
|     res.json({ students }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
|     const student = await getStudent(username); | ||||
| 
 | ||||
|     const user = await getStudent(username); | ||||
| 
 | ||||
|     if (!user) { | ||||
|         res.status(404).json({ | ||||
|             error: `User with username '${username}' not found.`, | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json(user); | ||||
|     res.json({ student }); | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
|     if (!userData.username || !userData.firstName || !userData.lastName) { | ||||
|         res.status(400).json({ | ||||
|             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); | ||||
|     const student = await createStudent(userData); | ||||
|     res.json({ student }); | ||||
| } | ||||
| 
 | ||||
| export async function deleteStudentHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         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); | ||||
|     const student = await deleteStudent(username); | ||||
|     res.json({ student }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
|     const username = req.params.id; | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const classes = await getStudentClasses(username, full); | ||||
| 
 | ||||
|     res.json({ classes: classes }); | ||||
|     res.json({ classes }); | ||||
| } | ||||
| 
 | ||||
| // TODO
 | ||||
|  | @ -103,33 +70,75 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro | |||
| // Have this assignment.
 | ||||
| export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
|     const username = req.params.id; | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const assignments = getStudentAssignments(username, full); | ||||
| 
 | ||||
|     res.json({ | ||||
|         assignments: assignments, | ||||
|     }); | ||||
|     res.json({ assignments }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
|     const username = req.params.id; | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const groups = await getStudentGroups(username, full); | ||||
| 
 | ||||
|     res.json({ | ||||
|         groups: groups, | ||||
|     }); | ||||
|     res.json({ groups }); | ||||
| } | ||||
| 
 | ||||
| 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'; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const submissions = await getStudentSubmissions(username, full); | ||||
| 
 | ||||
|     res.json({ | ||||
|         submissions: submissions, | ||||
|     }); | ||||
|     res.json({ 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, | ||||
|     getAllTeachers, | ||||
|     getClassesByTeacher, | ||||
|     getQuestionsByTeacher, | ||||
|     getJoinRequestsByClass, | ||||
|     getStudentsByTeacher, | ||||
|     getTeacher, | ||||
|     getTeacherQuestions, | ||||
|     updateClassJoinRequestStatus, | ||||
| } from '../services/teachers.js'; | ||||
| import { requireFields } from './error-helper.js'; | ||||
| import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | ||||
| 
 | ||||
| export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     const teachers = await getAllTeachers(full); | ||||
|     const teachers: TeacherDTO[] | string[] = await getAllTeachers(full); | ||||
| 
 | ||||
|     if (!teachers) { | ||||
|         res.status(404).json({ error: `Teacher not found.` }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json({ teachers: teachers }); | ||||
|     res.json({ teachers }); | ||||
| } | ||||
| 
 | ||||
| export async function getTeacherHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
|     const teacher = await getTeacher(username); | ||||
| 
 | ||||
|     const user = await getTeacher(username); | ||||
| 
 | ||||
|     if (!user) { | ||||
|         res.status(404).json({ | ||||
|             error: `Teacher '${username}' not found.`, | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json(user); | ||||
|     res.json({ teacher }); | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
|     if (!userData.username || !userData.firstName || !userData.lastName) { | ||||
|         res.status(400).json({ | ||||
|             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); | ||||
|     const teacher = await createTeacher(userData); | ||||
|     res.json({ teacher }); | ||||
| } | ||||
| 
 | ||||
| export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const deletedUser = await deleteTeacher(username); | ||||
|     if (!deletedUser) { | ||||
|         res.status(404).json({ | ||||
|             error: `User '${username}' not found.`, | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.status(200).json(deletedUser); | ||||
|     const teacher = await deleteTeacher(username); | ||||
|     res.json({ teacher }); | ||||
| } | ||||
| 
 | ||||
| export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const classes = await getClassesByTeacher(username, full); | ||||
| 
 | ||||
|     if (!classes) { | ||||
|         res.status(404).json({ error: 'Teacher not found' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json({ classes: classes }); | ||||
|     res.json({ classes }); | ||||
| } | ||||
| 
 | ||||
| export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const students = await getStudentsByTeacher(username, full); | ||||
| 
 | ||||
|     if (!students) { | ||||
|         res.status(404).json({ error: 'Teacher not found' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json({ students: students }); | ||||
|     res.json({ students }); | ||||
| } | ||||
| 
 | ||||
| export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     const full = req.query.full === 'true'; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     const questions = await getTeacherQuestions(username, full); | ||||
| 
 | ||||
|     res.json({ questions }); | ||||
| } | ||||
| 
 | ||||
|     const questions = await getQuestionsByTeacher(username, full); | ||||
| export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.query.username as string; | ||||
|     const classId = req.params.classId; | ||||
|     requireFields({ username, classId }); | ||||
| 
 | ||||
|     if (!questions) { | ||||
|         res.status(404).json({ error: 'Teacher not found' }); | ||||
|         return; | ||||
|     const joinRequests = await getJoinRequestsByClass(classId); | ||||
|     res.json({ joinRequests }); | ||||
| } | ||||
| 
 | ||||
|     res.json({ questions: questions }); | ||||
| 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 { ClassJoinRequest } from '../../entities/classes/class-join-request.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> { | ||||
|     public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> { | ||||
|         return this.findAll({ where: { requester: requester } }); | ||||
|     } | ||||
|     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> { | ||||
|         return this.deleteWhere({ requester: requester, class: clazz }); | ||||
|  |  | |||
|  | @ -54,4 +54,11 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | |||
|             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 { | ||||
|     title: string; | ||||
|     hruids: string[]; | ||||
| } | ||||
| import { Theme } from '@dwengo-1/common/interfaces/theme'; | ||||
| 
 | ||||
| export const themes: Theme[] = [ | ||||
|     { | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| 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 { 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 { | ||||
|         author: answer.author.username, | ||||
|         toQuestion: mapToQuestionId(answer.toQuestion), | ||||
|         sequenceNumber: answer.sequenceNumber, | ||||
|         toQuestion: mapToQuestionDTOId(answer.toQuestion), | ||||
|         sequenceNumber: answer.sequenceNumber!, | ||||
|     }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,16 +1,21 @@ | |||
| import { Question } from '../entities/questions/question.entity.js'; | ||||
| import { mapToStudentDTO } from './student.js'; | ||||
| 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. | ||||
|  */ | ||||
| export function mapToQuestionDTO(question: Question): QuestionDTO { | ||||
|     const learningObjectIdentifier = { | ||||
|         hruid: question.learningObjectHruid, | ||||
|         language: question.learningObjectLanguage, | ||||
|         version: question.learningObjectVersion, | ||||
|     }; | ||||
|     const learningObjectIdentifier = getLearningObjectIdentifier(question); | ||||
| 
 | ||||
|     return { | ||||
|         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 { | ||||
|         learningObjectIdentifier: question.learningObjectIdentifier, | ||||
|         learningObjectIdentifier, | ||||
|         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, | ||||
|     getStudentGroupsHandler, | ||||
|     getStudentHandler, | ||||
|     getStudentQuestionsHandler, | ||||
|     getStudentSubmissionsHandler, | ||||
| } from '../controllers/students.js'; | ||||
| import joinRequestRouter from './student-join-requests.js'; | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
|  | @ -17,30 +19,26 @@ router.get('/', getAllStudentsHandler); | |||
| 
 | ||||
| router.post('/', createStudentHandler); | ||||
| 
 | ||||
| router.delete('/', deleteStudentHandler); | ||||
| 
 | ||||
| router.delete('/:username', deleteStudentHandler); | ||||
| 
 | ||||
| // Information about a student's profile
 | ||||
| router.get('/:username', getStudentHandler); | ||||
| 
 | ||||
| // 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
 | ||||
| router.get('/:id/submissions', getStudentSubmissionsHandler); | ||||
| router.get('/:username/submissions', getStudentSubmissionsHandler); | ||||
| 
 | ||||
| // 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
 | ||||
| router.get('/:id/groups', getStudentGroupsHandler); | ||||
| router.get('/:username/groups', getStudentGroupsHandler); | ||||
| 
 | ||||
| // A list of questions a user has created
 | ||||
| router.get('/:id/questions', (_req, res) => { | ||||
|     res.json({ | ||||
|         questions: ['0'], | ||||
|     }); | ||||
| }); | ||||
| router.get('/:username/questions', getStudentQuestionsHandler); | ||||
| 
 | ||||
| router.use('/:username/joinRequests', joinRequestRouter); | ||||
| 
 | ||||
| export default router; | ||||
|  |  | |||
|  | @ -3,10 +3,12 @@ import { | |||
|     createTeacherHandler, | ||||
|     deleteTeacherHandler, | ||||
|     getAllTeachersHandler, | ||||
|     getStudentJoinRequestHandler, | ||||
|     getTeacherClassHandler, | ||||
|     getTeacherHandler, | ||||
|     getTeacherQuestionHandler, | ||||
|     getTeacherStudentHandler, | ||||
|     updateStudentJoinRequestHandler, | ||||
| } from '../controllers/teachers.js'; | ||||
| const router = express.Router(); | ||||
| 
 | ||||
|  | @ -15,8 +17,6 @@ router.get('/', getAllTeachersHandler); | |||
| 
 | ||||
| router.post('/', createTeacherHandler); | ||||
| 
 | ||||
| router.delete('/', deleteTeacherHandler); | ||||
| 
 | ||||
| router.get('/:username', getTeacherHandler); | ||||
| 
 | ||||
| router.delete('/:username', deleteTeacherHandler); | ||||
|  | @ -27,6 +27,10 @@ router.get('/:username/students', getTeacherStudentHandler); | |||
| 
 | ||||
| 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
 | ||||
| router.get('/:id/invitations', (_req, res) => { | ||||
|     res.json({ | ||||
|  |  | |||
|  | @ -3,12 +3,25 @@ import { mapToClassDTO } from '../interfaces/class.js'; | |||
| import { mapToStudentDTO } from '../interfaces/student.js'; | ||||
| import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.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 { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; | ||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||
| 
 | ||||
| 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[]> { | ||||
|     const classRepository = getClassRepository(); | ||||
|     const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| 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 { 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 { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||
| import { mapToStudent } from '../interfaces/student.js'; | ||||
|  | @ -17,13 +17,11 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea | |||
|         return []; | ||||
|     } | ||||
| 
 | ||||
|     const questionsDTO: QuestionDTO[] = questions.map(mapToQuestionDTO); | ||||
| 
 | ||||
|     if (full) { | ||||
|         return questionsDTO; | ||||
|         return questions.map(mapToQuestionDTO); | ||||
|     } | ||||
| 
 | ||||
|     return questionsDTO.map(mapToQuestionId); | ||||
|     return questions.map(mapToQuestionDTOId); | ||||
| } | ||||
| 
 | ||||
| async function fetchQuestion(questionId: QuestionId): Promise<Question | null> { | ||||
|  | @ -61,13 +59,11 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean | |||
|         return []; | ||||
|     } | ||||
| 
 | ||||
|     const answersDTO = answers.map(mapToAnswerDTO); | ||||
| 
 | ||||
|     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> { | ||||
|  |  | |||
|  | @ -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 { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | ||||
| import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js'; | ||||
| import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.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 { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; | ||||
| import { GroupDTO } from '@dwengo-1/common/interfaces/group'; | ||||
| import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; | ||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||
| import { getLogger } from '../logging/initalize.js'; | ||||
| import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||
| import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; | ||||
| 
 | ||||
| export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> { | ||||
|     const studentRepository = getStudentRepository(); | ||||
|     const students = await studentRepository.findAll(); | ||||
|     const users = await studentRepository.findAll(); | ||||
| 
 | ||||
|     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 user = await studentRepository.findByUsername(username); | ||||
|     return user ? mapToStudentDTO(user) : null; | ||||
| 
 | ||||
|     if (!user) { | ||||
|         throw new NotFoundException('Student with username not found'); | ||||
|     } | ||||
| 
 | ||||
| export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> { | ||||
|     return user; | ||||
| } | ||||
| 
 | ||||
| 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 newStudent = mapToStudent(userData); | ||||
|     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 user = await studentRepository.findByUsername(username); | ||||
|     const student = await fetchStudent(username); // Throws error if it does not exist
 | ||||
| 
 | ||||
|     if (!user) { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|     await studentRepository.deleteByUsername(username); | ||||
| 
 | ||||
|         return mapToStudentDTO(user); | ||||
|     } catch (e) { | ||||
|         getLogger().error(e); | ||||
|         return null; | ||||
|     } | ||||
|     return mapToStudentDTO(student); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentClasses(username: string, full: boolean): Promise<ClassDTO[] | string[]> { | ||||
|     const studentRepository = getStudentRepository(); | ||||
|     const student = await studentRepository.findByUsername(username); | ||||
| 
 | ||||
|     if (!student) { | ||||
|         return []; | ||||
|     } | ||||
|     const student = await fetchStudent(username); | ||||
| 
 | ||||
|     const classRepository = getClassRepository(); | ||||
|     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[]> { | ||||
|     const studentRepository = getStudentRepository(); | ||||
|     const student = await studentRepository.findByUsername(username); | ||||
| 
 | ||||
|     if (!student) { | ||||
|         return []; | ||||
|     } | ||||
|     const student = await fetchStudent(username); | ||||
| 
 | ||||
|     const classRepository = getClassRepository(); | ||||
|     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[]> { | ||||
|     const studentRepository = getStudentRepository(); | ||||
|     const student = await studentRepository.findByUsername(username); | ||||
| 
 | ||||
|     if (!student) { | ||||
|         return []; | ||||
|     } | ||||
|     const student = await fetchStudent(username); | ||||
| 
 | ||||
|     const groupRepository = getGroupRepository(); | ||||
|     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[]> { | ||||
|     const studentRepository = getStudentRepository(); | ||||
|     const student = await studentRepository.findByUsername(username); | ||||
| 
 | ||||
|     if (!student) { | ||||
|         return []; | ||||
|     } | ||||
|     const student = await fetchStudent(username); | ||||
| 
 | ||||
|     const submissionRepository = getSubmissionRepository(); | ||||
|     const submissions = await submissionRepository.findAllSubmissionsForStudent(student); | ||||
|  | @ -122,3 +115,66 @@ export async function getStudentSubmissions(username: string, full: boolean): Pr | |||
| 
 | ||||
|     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 { getClassStudents } from './classes.js'; | ||||
| import { mapToQuestionDTO, mapToQuestionId } from '../interfaces/question.js'; | ||||
| import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.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 { ClassDTO } from '@dwengo-1/common/interfaces/class'; | ||||
| 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'; | ||||
| import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; | ||||
| 
 | ||||
| export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> { | ||||
|     const teacherRepository = getTeacherRepository(); | ||||
|     const teachers = await teacherRepository.findAll(); | ||||
|     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||
|     const users: Teacher[] = await teacherRepository.findAll(); | ||||
| 
 | ||||
|     if (full) { | ||||
|         return teachers.map(mapToTeacherDTO); | ||||
|         return users.map(mapToTeacherDTO); | ||||
|     } | ||||
|     return users.map((user) => user.username); | ||||
| } | ||||
| 
 | ||||
|     return teachers.map((teacher) => teacher.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'); | ||||
|     } | ||||
| 
 | ||||
| export async function getTeacher(username: string): Promise<TeacherDTO | null> { | ||||
|     const teacherRepository = getTeacherRepository(); | ||||
|     const user = await teacherRepository.findByUsername(username); | ||||
|     return user ? mapToTeacherDTO(user) : null; | ||||
|     return user; | ||||
| } | ||||
| 
 | ||||
| export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> { | ||||
|     const teacherRepository = getTeacherRepository(); | ||||
| export async function getTeacher(username: string): Promise<TeacherDTO> { | ||||
|     const user: Teacher = await fetchTeacher(username); | ||||
|     return mapToTeacherDTO(user); | ||||
| } | ||||
| 
 | ||||
| export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO> { | ||||
|     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||
| 
 | ||||
|     const newTeacher = mapToTeacher(userData); | ||||
|     await teacherRepository.save(newTeacher, { preventOverwrite: true }); | ||||
| 
 | ||||
|     return mapToTeacherDTO(newTeacher); | ||||
| } | ||||
| 
 | ||||
| export async function deleteTeacher(username: string): Promise<TeacherDTO | null> { | ||||
|     const teacherRepository = getTeacherRepository(); | ||||
| export async function deleteTeacher(username: string): Promise<TeacherDTO> { | ||||
|     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||
| 
 | ||||
|     const user = await teacherRepository.findByUsername(username); | ||||
|     const teacher = await fetchTeacher(username); // Throws error if it does not exist
 | ||||
| 
 | ||||
|     if (!user) { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|     await teacherRepository.deleteByUsername(username); | ||||
| 
 | ||||
|         return mapToTeacherDTO(user); | ||||
|     } catch (e) { | ||||
|         getLogger().error(e); | ||||
|         return null; | ||||
|     } | ||||
|     return mapToTeacherDTO(teacher); | ||||
| } | ||||
| 
 | ||||
| export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[] | null> { | ||||
|     const teacherRepository = getTeacherRepository(); | ||||
|     const teacher = await teacherRepository.findByUsername(username); | ||||
|     if (!teacher) { | ||||
|         return null; | ||||
|     } | ||||
| async function fetchClassesByTeacher(username: string): Promise<ClassDTO[]> { | ||||
|     const teacher: Teacher = await fetchTeacher(username); | ||||
| 
 | ||||
|     const classRepository = getClassRepository(); | ||||
|     const classes = await classRepository.findByTeacher(teacher); | ||||
|     const classRepository: ClassRepository = getClassRepository(); | ||||
|     const classes: Class[] = await classRepository.findByTeacher(teacher); | ||||
|     return classes.map(mapToClassDTO); | ||||
| } | ||||
| 
 | ||||
| export async function getClassesByTeacher(username: string, full: boolean): Promise<ClassDTO[] | string[] | null> { | ||||
|     const classes = await fetchClassesByTeacher(username); | ||||
| 
 | ||||
|     if (!classes) { | ||||
|         return null; | ||||
|     } | ||||
| export async function getClassesByTeacher(username: string, full: boolean): Promise<ClassDTO[] | string[]> { | ||||
|     const classes: ClassDTO[] = await fetchClassesByTeacher(username); | ||||
| 
 | ||||
|     if (full) { | ||||
|         return classes; | ||||
|     } | ||||
| 
 | ||||
|     return classes.map((cls) => cls.id); | ||||
| } | ||||
| 
 | ||||
| export async function fetchStudentsByTeacher(username: string): Promise<StudentDTO[] | null> { | ||||
|     const classes = (await getClassesByTeacher(username, false)) as string[]; | ||||
| export async function getStudentsByTeacher(username: string, full: boolean): Promise<StudentDTO[] | string[]> { | ||||
|     const classes: ClassDTO[] = await fetchClassesByTeacher(username); | ||||
| 
 | ||||
|     if (!classes) { | ||||
|         return null; | ||||
|     if (!classes || classes.length === 0) { | ||||
|         return []; | ||||
|     } | ||||
| 
 | ||||
|     return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat(); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentsByTeacher(username: string, full: boolean): Promise<StudentDTO[] | string[] | null> { | ||||
|     const students = await fetchStudentsByTeacher(username); | ||||
| 
 | ||||
|     if (!students) { | ||||
|         return null; | ||||
|     } | ||||
|     const classIds: string[] = classes.map((cls) => cls.id); | ||||
| 
 | ||||
|     const students: StudentDTO[] = (await Promise.all(classIds.map(async (id) => getClassStudents(id)))).flat(); | ||||
|     if (full) { | ||||
|         return students; | ||||
|     } | ||||
| 
 | ||||
|     return students.map((student) => student.username); | ||||
| } | ||||
| 
 | ||||
| export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[] | null> { | ||||
|     const teacherRepository = getTeacherRepository(); | ||||
|     const teacher = await teacherRepository.findByUsername(username); | ||||
|     if (!teacher) { | ||||
|         return null; | ||||
|     } | ||||
| export async function getTeacherQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> { | ||||
|     const teacher: Teacher = await fetchTeacher(username); | ||||
| 
 | ||||
|     // Find all learning objects that this teacher manages
 | ||||
|     const learningObjectRepository = getLearningObjectRepository(); | ||||
|     const learningObjects = await learningObjectRepository.findAllByTeacher(teacher); | ||||
|     const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository(); | ||||
|     const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher); | ||||
| 
 | ||||
|     if (!learningObjects || learningObjects.length === 0) { | ||||
|         return []; | ||||
|     } | ||||
| 
 | ||||
|     // Fetch all questions related to these learning objects
 | ||||
|     const questionRepository = getQuestionRepository(); | ||||
|     const questions = await questionRepository.findAllByLearningObjects(learningObjects); | ||||
|     const questionRepository: QuestionRepository = getQuestionRepository(); | ||||
|     const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects); | ||||
| 
 | ||||
|     if (full) { | ||||
|         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; | ||||
|     return questions.map(mapToQuestionDTOId); | ||||
| } | ||||
| 
 | ||||
|     if (full) { | ||||
|         return questions; | ||||
| 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'); | ||||
|     } | ||||
| 
 | ||||
|     return questions.map(mapToQuestionId); | ||||
|     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 { 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[] { | ||||
|     const student01 = em.create(Student, { | ||||
|         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]; | ||||
|     return TEST_STUDENTS.map((data) => em.create(Student, data)); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										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 type { Theme } from "@dwengo-1/interfaces/theme"; | ||||
| 
 | ||||
| export class ThemeController extends BaseController { | ||||
|     constructor() { | ||||
|         super("theme"); | ||||
|     } | ||||
| 
 | ||||
|     async getAll(language: string | null = null): Promise<unknown> { | ||||
|     async getAll(language: string | null = null): Promise<Theme[]> { | ||||
|         const query = language ? { language } : undefined; | ||||
|         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 { getThemeController } from "@/controllers/controllers"; | ||||
| 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(); | ||||
| 
 | ||||
| export function useThemeQuery(language: MaybeRefOrGetter<string>): UseQueryReturnType<Theme[], Error> { | ||||
| export function useThemeQuery(language: MaybeRefOrGetter<string | undefined>): UseQueryReturnType<Theme[], Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: ["themes", language], | ||||
|         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({ | ||||
|         queryKey: ["theme-hruids", themeKey], | ||||
|         queryFn: async () => themeController.getHruidsByKey(themeKey!), | ||||
|         queryFn: async () => themeController.getHruidsByKey(toValue(themeKey)!), | ||||
|         enabled: Boolean(themeKey), | ||||
|     }); | ||||
| } | ||||
|  |  | |||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl