Merge pull request #142 from SELab-2/feat/user-routes
Some checks failed
Lint / Run linters (push) Has been cancelled

feat: endpoints finaliseren users
This commit is contained in:
Gabriellvl 2025-04-03 11:17:28 +02:00 committed by GitHub
commit 2b2c97a82d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1443 additions and 374 deletions

View 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);
}
}

View file

@ -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 });
}

View file

@ -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);
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<void> {
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<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 });
}

View file

@ -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 });

View file

@ -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
});
}
}

View file

@ -1,7 +1,4 @@
export interface Theme {
title: string;
hruids: string[];
}
import { Theme } from '@dwengo-1/common/interfaces/theme';
export const themes: Theme[] = [
{

View file

@ -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!,
};
}

View file

@ -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!,
};
}

View 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,
});
}

View 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;

View file

@ -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;

View file

@ -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({

View file

@ -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'] });

View file

@ -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> {

View file

@ -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');
}
return user;
}
export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> {
export async function getStudent(username: string): Promise<StudentDTO> {
const user = await fetchStudent(username);
return mapToStudentDTO(user);
}
export async function createStudent(userData: StudentDTO): Promise<StudentDTO> {
const studentRepository = getStudentRepository();
const 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;
}
await studentRepository.deleteByUsername(username);
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);
}

View file

@ -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);
}
export async function fetchTeacher(username: string): Promise<Teacher> {
const teacherRepository: TeacherRepository = getTeacherRepository();
const user: Teacher | null = await teacherRepository.findByUsername(username);
if (!user) {
throw new NotFoundException('Teacher with username not found');
}
return teachers.map((teacher) => teacher.username);
return user;
}
export async function getTeacher(username: string): Promise<TeacherDTO | null> {
const teacherRepository = getTeacherRepository();
const user = await teacherRepository.findByUsername(username);
return user ? mapToTeacherDTO(user) : null;
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 | null> {
const teacherRepository = getTeacherRepository();
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;
}
await teacherRepository.deleteByUsername(username);
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);
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;
}
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<ClassJoinRequestDTO[]> {
const classRepository: ClassRepository = getClassRepository();
const cls: Class | null = await classRepository.findById(classId);
if (!cls) {
throw new NotFoundException('Class with id not found');
}
const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository();
const requests: ClassJoinRequest[] = await requestRepo.findAllOpenRequestsTo(cls);
return requests.map(mapToStudentRequestDTO);
}
export async function updateClassJoinRequestStatus(studentUsername: string, classId: string, accepted = true): Promise<ClassJoinRequestDTO> {
const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository();
const classRepo: ClassRepository = getClassRepository();
const student: Student = await fetchStudent(studentUsername);
const cls: Class | null = await classRepo.findById(classId);
if (!cls) {
throw new NotFoundException('Class not found');
}
const request: ClassJoinRequest | null = await requestRepo.findByStudentAndClass(student, cls);
if (!request) {
throw new NotFoundException('Join request not found');
}
request.status = accepted ? ClassJoinRequestStatus.Accepted : ClassJoinRequestStatus.Declined;
await requestRepo.save(request);
return mapToStudentRequestDTO(request);
}

View 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);
});
});

View 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();
});
});

View file

@ -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));
}

View 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;
}

View file

@ -0,0 +1,4 @@
export interface Theme {
title: string;
hruids: string[];
}

View file

@ -0,0 +1,5 @@
import type { AssignmentDTO } from "@dwengo-1/interfaces/assignment";
export interface AssignmentsResponse {
assignments: AssignmentDTO[];
} // TODO ID

View file

@ -0,0 +1,5 @@
import type { ClassDTO } from "@dwengo-1/interfaces/class";
export interface ClassesResponse {
classes: ClassDTO[] | string[];
}

View file

@ -0,0 +1,5 @@
import type { GroupDTO } from "@dwengo-1/interfaces/group";
export interface GroupsResponse {
groups: GroupDTO[];
} // | TODO id

View file

@ -0,0 +1,5 @@
import type { QuestionDTO, QuestionId } from "@dwengo-1/interfaces/question";
export interface QuestionsResponse {
questions: QuestionDTO[] | QuestionId[];
}

View 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}`);
}
}

View file

@ -0,0 +1,5 @@
import { type SubmissionDTO, SubmissionDTOId } from "@dwengo-1/interfaces/submission";
export interface SubmissionsResponse {
submissions: SubmissionDTO[] | SubmissionDTOId[];
}

View 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`);}
}

View file

@ -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);
}

View 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) });
},
});
}

View 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) });
},
});
}

View file

@ -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),
});
}