diff --git a/backend/src/controllers/error-helper.ts b/backend/src/controllers/error-helper.ts new file mode 100644 index 00000000..a902560f --- /dev/null +++ b/backend/src/controllers/error-helper.ts @@ -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): 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); + } +} diff --git a/backend/src/controllers/students.ts b/backend/src/controllers/students.ts index 54aae143..51488a2a 100644 --- a/backend/src/controllers/students.ts +++ b/backend/src/controllers/students.ts @@ -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 { 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 { 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 { + 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 { 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 { 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 { 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 { 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 { - 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 { + 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 { + 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 { + const username = req.params.username; + requireFields({ username }); + + const requests = await getJoinRequestsByStudent(username); + res.json({ requests }); +} + +export async function getStudentRequestHandler(req: Request, res: Response): Promise { + 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 { + const username = req.params.username; + const classId = req.params.classId; + requireFields({ username, classId }); + + const request = await deleteClassJoinRequest(username, classId); + res.json({ request }); } diff --git a/backend/src/controllers/teachers.ts b/backend/src/controllers/teachers.ts index c3894bd6..9275ca92 100644 --- a/backend/src/controllers/teachers.ts +++ b/backend/src/controllers/teachers.ts @@ -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 { 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 { 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 { + 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 { 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 { 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 { 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 { 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); - const questions = await getQuestionsByTeacher(username, full); - - if (!questions) { - res.status(404).json({ error: 'Teacher not found' }); - return; - } - - res.json({ questions: questions }); + res.json({ questions }); +} + +export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise { + const username = req.query.username as string; + const classId = req.params.classId; + requireFields({ username, classId }); + + const joinRequests = await getJoinRequestsByClass(classId); + res.json({ joinRequests }); +} + +export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise { + 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 }); } diff --git a/backend/src/data/classes/class-join-request-repository.ts b/backend/src/data/classes/class-join-request-repository.ts index 1cd0288c..0d9ab6e1 100644 --- a/backend/src/data/classes/class-join-request-repository.ts +++ b/backend/src/data/classes/class-join-request-repository.ts @@ -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 { public async findAllRequestsBy(requester: Student): Promise { return this.findAll({ where: { requester: requester } }); } public async findAllOpenRequestsTo(clazz: Class): Promise { - 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 { + return this.findOne({ requester, class: clazz }); } public async deleteBy(requester: Student, clazz: Class): Promise { return this.deleteWhere({ requester: requester, class: clazz }); diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 596b562c..2d165abc 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -54,4 +54,11 @@ export class QuestionRepository extends DwengoEntityRepository { orderBy: { timestamp: 'ASC' }, }); } + + public async findAllByAuthor(author: Student): Promise { + return this.findAll({ + where: { author }, + orderBy: { timestamp: 'DESC' }, // New to old + }); + } } diff --git a/backend/src/data/themes.ts b/backend/src/data/themes.ts index b0fc930c..0a2272e6 100644 --- a/backend/src/data/themes.ts +++ b/backend/src/data/themes.ts @@ -1,7 +1,4 @@ -export interface Theme { - title: string; - hruids: string[]; -} +import { Theme } from '@dwengo-1/common/interfaces/theme'; export const themes: Theme[] = [ { diff --git a/backend/src/interfaces/answer.ts b/backend/src/interfaces/answer.ts index 5599839f..1f0d0625 100644 --- a/backend/src/interfaces/answer.ts +++ b/backend/src/interfaces/answer.ts @@ -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!, }; } diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 57778bac..48d64f11 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -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!, }; } diff --git a/backend/src/interfaces/student-request.ts b/backend/src/interfaces/student-request.ts new file mode 100644 index 00000000..d97f5eb5 --- /dev/null +++ b/backend/src/interfaces/student-request.ts @@ -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, + }); +} diff --git a/backend/src/routes/student-join-requests.ts b/backend/src/routes/student-join-requests.ts new file mode 100644 index 00000000..daf79f09 --- /dev/null +++ b/backend/src/routes/student-join-requests.ts @@ -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; diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index d58c2adc..0f5d5349 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -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; diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index 3782a6ca..a6106a80 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -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({ diff --git a/backend/src/services/classes.ts b/backend/src/services/classes.ts index fccbbee0..754277cf 100644 --- a/backend/src/services/classes.ts +++ b/backend/src/services/classes.ts @@ -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 { + 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 { const classRepository = getClassRepository(); const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index f92efae0..319061c5 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -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 { @@ -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 { diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 968b556b..dc40e468 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -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 { 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 { +export async function fetchStudent(username: string): Promise { const studentRepository = getStudentRepository(); const user = await studentRepository.findByUsername(username); - return user ? mapToStudentDTO(user) : null; + + if (!user) { + throw new NotFoundException('Student with username not found'); + } + + return user; } -export async function createStudent(userData: StudentDTO): Promise { +export async function getStudent(username: string): Promise { + const user = await fetchStudent(username); + return mapToStudentDTO(user); +} + +export async function createStudent(userData: StudentDTO): Promise { 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 { +export async function deleteStudent(username: string): Promise { 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; - } + await studentRepository.deleteByUsername(username); + return mapToStudentDTO(student); } export async function getStudentClasses(username: string, full: boolean): Promise { - 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 { - 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 { - 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 { - 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 { + 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 { + 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 { + 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 { + 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 { + 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); +} diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index 3986462b..1b7643fb 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -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 { - 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); +} + +export async function fetchTeacher(username: string): Promise { + const teacherRepository: TeacherRepository = getTeacherRepository(); + const user: Teacher | null = await teacherRepository.findByUsername(username); + + if (!user) { + throw new NotFoundException('Teacher with username not found'); } - return teachers.map((teacher) => teacher.username); + return user; } -export async function getTeacher(username: string): Promise { - const teacherRepository = getTeacherRepository(); - const user = await teacherRepository.findByUsername(username); - return user ? mapToTeacherDTO(user) : null; +export async function getTeacher(username: string): Promise { + const user: Teacher = await fetchTeacher(username); + return mapToTeacherDTO(user); } -export async function createTeacher(userData: TeacherDTO): Promise { - const teacherRepository = getTeacherRepository(); +export async function createTeacher(userData: TeacherDTO): Promise { + const teacherRepository: TeacherRepository = getTeacherRepository(); const newTeacher = mapToTeacher(userData); await teacherRepository.save(newTeacher, { preventOverwrite: true }); - return mapToTeacherDTO(newTeacher); } -export async function deleteTeacher(username: string): Promise { - const teacherRepository = getTeacherRepository(); +export async function deleteTeacher(username: string): Promise { + 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; - } + await teacherRepository.deleteByUsername(username); + return mapToTeacherDTO(teacher); } -export async function fetchClassesByTeacher(username: string): Promise { - const teacherRepository = getTeacherRepository(); - const teacher = await teacherRepository.findByUsername(username); - if (!teacher) { - return null; - } +async function fetchClassesByTeacher(username: string): Promise { + 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 { - const classes = await fetchClassesByTeacher(username); - - if (!classes) { - return null; - } +export async function getClassesByTeacher(username: string, full: boolean): Promise { + const classes: ClassDTO[] = await fetchClassesByTeacher(username); if (full) { return classes; } - return classes.map((cls) => cls.id); } -export async function fetchStudentsByTeacher(username: string): Promise { - const classes = (await getClassesByTeacher(username, false)) as string[]; +export async function getStudentsByTeacher(username: string, full: boolean): Promise { + 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 { - 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 { - const teacherRepository = getTeacherRepository(); - const teacher = await teacherRepository.findByUsername(username); - if (!teacher) { - return null; - } +export async function getTeacherQuestions(username: string, full: boolean): Promise { + 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); - - return questions.map(mapToQuestionDTO); -} - -export async function getQuestionsByTeacher(username: string, full: boolean): Promise { - const questions = await fetchTeacherQuestions(username); - - if (!questions) { - return null; - } + const questionRepository: QuestionRepository = getQuestionRepository(); + const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects); if (full) { - return questions; + return questions.map(mapToQuestionDTO); } - return questions.map(mapToQuestionId); + return questions.map(mapToQuestionDTOId); +} + +export async function getJoinRequestsByClass(classId: string): Promise { + const classRepository: ClassRepository = getClassRepository(); + const cls: Class | null = await classRepository.findById(classId); + + if (!cls) { + throw new NotFoundException('Class with id not found'); + } + + const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository(); + const requests: ClassJoinRequest[] = await requestRepo.findAllOpenRequestsTo(cls); + return requests.map(mapToStudentRequestDTO); +} + +export async function updateClassJoinRequestStatus(studentUsername: string, classId: string, accepted = true): Promise { + 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); } diff --git a/backend/tests/controllers/students.test.ts b/backend/tests/controllers/students.test.ts new file mode 100644 index 00000000..93f35c48 --- /dev/null +++ b/backend/tests/controllers/students.test.ts @@ -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; + let res: Partial; + + 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); + }); +}); diff --git a/backend/tests/controllers/teachers.test.ts b/backend/tests/controllers/teachers.test.ts new file mode 100644 index 00000000..bee23987 --- /dev/null +++ b/backend/tests/controllers/teachers.test.ts @@ -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; + let res: Partial; + + 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(); + }); +}); diff --git a/backend/tests/test_assets/users/students.testdata.ts b/backend/tests/test_assets/users/students.testdata.ts index 7d69db9e..5cd75787 100644 --- a/backend/tests/test_assets/users/students.testdata.ts +++ b/backend/tests/test_assets/users/students.testdata.ts @@ -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)); } diff --git a/common/src/interfaces/class-join-request.ts b/common/src/interfaces/class-join-request.ts new file mode 100644 index 00000000..6787998b --- /dev/null +++ b/common/src/interfaces/class-join-request.ts @@ -0,0 +1,8 @@ +import { StudentDTO } from './student'; +import { ClassJoinRequestStatus } from '../util/class-join-request'; + +export interface ClassJoinRequestDTO { + requester: StudentDTO; + class: string; + status: ClassJoinRequestStatus; +} diff --git a/common/src/interfaces/theme.ts b/common/src/interfaces/theme.ts new file mode 100644 index 00000000..5334b775 --- /dev/null +++ b/common/src/interfaces/theme.ts @@ -0,0 +1,4 @@ +export interface Theme { + title: string; + hruids: string[]; +} diff --git a/frontend/src/controllers/assignments.ts b/frontend/src/controllers/assignments.ts new file mode 100644 index 00000000..5fd5090a --- /dev/null +++ b/frontend/src/controllers/assignments.ts @@ -0,0 +1,5 @@ +import type { AssignmentDTO } from "@dwengo-1/interfaces/assignment"; + +export interface AssignmentsResponse { + assignments: AssignmentDTO[]; +} // TODO ID diff --git a/frontend/src/controllers/classes.ts b/frontend/src/controllers/classes.ts new file mode 100644 index 00000000..d2d95ed5 --- /dev/null +++ b/frontend/src/controllers/classes.ts @@ -0,0 +1,5 @@ +import type { ClassDTO } from "@dwengo-1/interfaces/class"; + +export interface ClassesResponse { + classes: ClassDTO[] | string[]; +} diff --git a/frontend/src/controllers/groups.ts b/frontend/src/controllers/groups.ts new file mode 100644 index 00000000..d6738e04 --- /dev/null +++ b/frontend/src/controllers/groups.ts @@ -0,0 +1,5 @@ +import type { GroupDTO } from "@dwengo-1/interfaces/group"; + +export interface GroupsResponse { + groups: GroupDTO[]; +} // | TODO id diff --git a/frontend/src/controllers/questions.ts b/frontend/src/controllers/questions.ts new file mode 100644 index 00000000..9b0182de --- /dev/null +++ b/frontend/src/controllers/questions.ts @@ -0,0 +1,5 @@ +import type { QuestionDTO, QuestionId } from "@dwengo-1/interfaces/question"; + +export interface QuestionsResponse { + questions: QuestionDTO[] | QuestionId[]; +} diff --git a/frontend/src/controllers/students.ts b/frontend/src/controllers/students.ts new file mode 100644 index 00000000..b36f1d5a --- /dev/null +++ b/frontend/src/controllers/students.ts @@ -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 { + return this.get("/", { full }); + } + + async getByUsername(username: string): Promise { + return this.get(`/${username}`); + } + + async createStudent(data: StudentDTO): Promise { + return this.post("/", data); + } + + async deleteStudent(username: string): Promise { + return this.delete(`/${username}`); + } + + async getClasses(username: string, full = true): Promise { + return this.get(`/${username}/classes`, { full }); + } + + async getAssignments(username: string, full = true): Promise { + return this.get(`/${username}/assignments`, { full }); + } + + async getGroups(username: string, full = true): Promise { + return this.get(`/${username}/groups`, { full }); + } + + async getSubmissions(username: string): Promise { + return this.get(`/${username}/submissions`); + } + + async getQuestions(username: string, full = true): Promise { + return this.get(`/${username}/questions`, { full }); + } + + async getJoinRequests(username: string): Promise { + return this.get(`/${username}/joinRequests`); + } + + async getJoinRequest(username: string, classId: string): Promise { + return this.get(`/${username}/joinRequests/${classId}`); + } + + async createJoinRequest(username: string, classId: string): Promise { + return this.post(`/${username}/joinRequests}`, classId); + } + + async deleteJoinRequest(username: string, classId: string): Promise { + return this.delete(`/${username}/joinRequests/${classId}`); + } +} diff --git a/frontend/src/controllers/submissions.ts b/frontend/src/controllers/submissions.ts new file mode 100644 index 00000000..99b6ba8d --- /dev/null +++ b/frontend/src/controllers/submissions.ts @@ -0,0 +1,5 @@ +import { type SubmissionDTO, SubmissionDTOId } from "@dwengo-1/interfaces/submission"; + +export interface SubmissionsResponse { + submissions: SubmissionDTO[] | SubmissionDTOId[]; +} diff --git a/frontend/src/controllers/teachers.ts b/frontend/src/controllers/teachers.ts new file mode 100644 index 00000000..e0d38a6c --- /dev/null +++ b/frontend/src/controllers/teachers.ts @@ -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 { + return this.get("/", { full }); + } + + async getByUsername(username: string): Promise { + return this.get(`/${username}`); + } + + async createTeacher(data: TeacherDTO): Promise { + return this.post("/", data); + } + + async deleteTeacher(username: string): Promise { + return this.delete(`/${username}`); + } + + async getClasses(username: string, full = false): Promise { + return this.get(`/${username}/classes`, { full }); + } + + async getStudents(username: string, full = false): Promise { + return this.get(`/${username}/students`, { full }); + } + + async getQuestions(username: string, full = false): Promise { + return this.get(`/${username}/questions`, { full }); + } + + async getStudentJoinRequests(username: string, classId: string): Promise { + return this.get(`/${username}/joinRequests/${classId}`); + } + + async updateStudentJoinRequest( + teacherUsername: string, + classId: string, + studentUsername: string, + accepted: boolean, + ): Promise { + return this.put( + `/${teacherUsername}/joinRequests/${classId}/${studentUsername}`, + accepted, + ); + } + + // GetInvitations(id: string) {return this.get<{ invitations: string[] }>(`/${id}/invitations`);} +} diff --git a/frontend/src/controllers/themes.ts b/frontend/src/controllers/themes.ts index d6c8be98..bb76249d 100644 --- a/frontend/src/controllers/themes.ts +++ b/frontend/src/controllers/themes.ts @@ -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 { + async getAll(language: string | null = null): Promise { const query = language ? { language } : undefined; return this.get("/", query); } diff --git a/frontend/src/queries/students.ts b/frontend/src/queries/students.ts new file mode 100644 index 00000000..822083d9 --- /dev/null +++ b/frontend/src/queries/students.ts @@ -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 = true): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentsQueryKey(toValue(full))), + queryFn: async () => studentController.getAll(toValue(full)), + }); +} + +export function useStudentQuery( + username: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentQueryKey(toValue(username)!)), + queryFn: async () => studentController.getByUsername(toValue(username)!), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useStudentClassesQuery( + username: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + 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, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + 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, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + 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, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentSubmissionsQueryKey(toValue(username)!)), + queryFn: async () => studentController.getSubmissions(toValue(username)!), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useStudentQuestionsQuery( + username: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + 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, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentJoinRequestsQueryKey(toValue(username)!)), + queryFn: async () => studentController.getJoinRequests(toValue(username)!), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useStudentJoinRequestQuery( + username: MaybeRefOrGetter, + classId: MaybeRefOrGetter, +): UseQueryReturnType { + 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 { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data) => studentController.createStudent(data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["students"] }); + }, + }); +} + +export function useDeleteStudentMutation(): UseMutationReturnType { + 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) }); + }, + }); +} diff --git a/frontend/src/queries/teachers.ts b/frontend/src/queries/teachers.ts new file mode 100644 index 00000000..778b2cba --- /dev/null +++ b/frontend/src/queries/teachers.ts @@ -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 = false): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => teachersQueryKey(toValue(full))), + queryFn: async () => teacherController.getAll(toValue(full)), + }); +} + +export function useTeacherQuery( + username: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => teacherQueryKey(toValue(username)!)), + queryFn: async () => teacherController.getByUsername(toValue(username)!), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useTeacherClassesQuery( + username: MaybeRefOrGetter, + full: MaybeRefOrGetter = false, +): UseQueryReturnType { + 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, + full: MaybeRefOrGetter = false, +): UseQueryReturnType { + 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, + full: MaybeRefOrGetter = false, +): UseQueryReturnType { + 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, + classId: MaybeRefOrGetter, +): UseQueryReturnType { + 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 { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: TeacherDTO) => teacherController.createTeacher(data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["teachers"] }); + }, + }); +} + +export function useDeleteTeacherMutation(): UseMutationReturnType { + 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) }); + }, + }); +} diff --git a/frontend/src/queries/themes.ts b/frontend/src/queries/themes.ts index a9c50f23..c3be25ae 100644 --- a/frontend/src/queries/themes.ts +++ b/frontend/src/queries/themes.ts @@ -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): UseQueryReturnType { +export function useThemeQuery(language: MaybeRefOrGetter): UseQueryReturnType { return useQuery({ queryKey: ["themes", language], queryFn: async () => { @@ -16,10 +16,12 @@ export function useThemeQuery(language: MaybeRefOrGetter): UseQueryRetur }); } -export function useThemeHruidsQuery(themeKey: string | null): UseQueryReturnType { +export function useThemeHruidsQuery( + themeKey: MaybeRefOrGetter, +): UseQueryReturnType { return useQuery({ queryKey: ["theme-hruids", themeKey], - queryFn: async () => themeController.getHruidsByKey(themeKey!), + queryFn: async () => themeController.getHruidsByKey(toValue(themeKey)!), enabled: Boolean(themeKey), }); }