From 12c1505ba79454f65b3d2ce6b04b07e62b68de5d Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Sun, 6 Apr 2025 23:08:11 +0200 Subject: [PATCH 01/71] feat(backend): Groep aan primaire sleutel van submissions en questions toegevoegd. --- backend/src/data/questions/question-repository.ts | 4 +++- .../src/entities/assignments/submission.entity.ts | 12 +++++++----- backend/src/entities/questions/question.entity.ts | 3 +++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 2d165abc..087246b6 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -5,12 +5,13 @@ import { Student } from '../../entities/users/student.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; export class QuestionRepository extends DwengoEntityRepository { - public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise { + public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; group: Group, content: string }): Promise { const questionEntity = this.create({ learningObjectHruid: question.loId.hruid, learningObjectLanguage: question.loId.language, learningObjectVersion: question.loId.version, author: question.author, + group: question.group, content: question.content, timestamp: new Date(), }); @@ -18,6 +19,7 @@ export class QuestionRepository extends DwengoEntityRepository { questionEntity.learningObjectLanguage = question.loId.language; questionEntity.learningObjectVersion = question.loId.version; questionEntity.author = question.author; + questionEntity.group = question.group; questionEntity.content = question.content; return this.insert(questionEntity); } diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index 80b9a8fb..1f500890 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -21,6 +21,12 @@ export class Submission { @PrimaryKey({ type: 'integer', autoincrement: true }) submissionNumber?: number; + @ManyToOne({ + entity: () => Group, + primary: true + }) + onBehalfOf: Group; + @ManyToOne({ entity: () => Student, }) @@ -29,11 +35,7 @@ export class Submission { @Property({ type: 'datetime' }) submissionTime!: Date; - @ManyToOne({ - entity: () => Group, - nullable: true, - }) - onBehalfOf?: Group; + @Property({ type: 'json' }) content!: string; diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 5e691f70..8ee2c70e 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -20,6 +20,9 @@ export class Question { @PrimaryKey({ type: 'integer', autoincrement: true }) sequenceNumber?: number; + @ManyToOne({ entity: () => Group, primary: true }) + inGroup: Group; + @ManyToOne({ entity: () => Student, }) From f9b83bc4afafa24ef89db07a5bc9bb0ecab5c515 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Sun, 6 Apr 2025 23:36:23 +0200 Subject: [PATCH 02/71] fix(backend): Services en controllers aan gewijzigde primaire sleutel van Question en Submission aangepast. --- backend/src/controllers/questions.ts | 2 +- backend/src/data/questions/question-repository.ts | 7 ++++--- backend/src/entities/questions/question.entity.ts | 3 ++- backend/src/services/questions.ts | 1 + common/src/interfaces/question.ts | 3 +++ common/src/interfaces/submission.ts | 2 +- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index b5b764ac..54b50fa9 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -88,7 +88,7 @@ export async function getQuestionAnswersHandler(req: Request, res: Response): Pr export async function createQuestionHandler(req: Request, res: Response): Promise { const questionDTO = req.body as QuestionDTO; - if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) { + if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.inGroup || !questionDTO.content) { res.status(400).json({ error: 'Missing required fields: identifier and content' }); return; } diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 087246b6..51df5afe 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -3,15 +3,16 @@ import { Question } from '../../entities/questions/question.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { Student } from '../../entities/users/student.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; +import {Group} from "../../entities/assignments/group.entity"; export class QuestionRepository extends DwengoEntityRepository { - public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; group: Group, content: string }): Promise { + public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group, content: string }): Promise { const questionEntity = this.create({ learningObjectHruid: question.loId.hruid, learningObjectLanguage: question.loId.language, learningObjectVersion: question.loId.version, author: question.author, - group: question.group, + inGroup: question.inGroup, content: question.content, timestamp: new Date(), }); @@ -19,7 +20,7 @@ export class QuestionRepository extends DwengoEntityRepository { questionEntity.learningObjectLanguage = question.loId.language; questionEntity.learningObjectVersion = question.loId.version; questionEntity.author = question.author; - questionEntity.group = question.group; + questionEntity.inGroup = question.inGroup; questionEntity.content = question.content; return this.insert(questionEntity); } diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 8ee2c70e..bb36e8a3 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -2,6 +2,7 @@ import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; import { Student } from '../users/student.entity.js'; import { QuestionRepository } from '../../data/questions/question-repository.js'; import { Language } from '@dwengo-1/common/util/language'; +import {Group} from "../assignments/group.entity"; @Entity({ repository: () => QuestionRepository }) export class Question { @@ -21,7 +22,7 @@ export class Question { sequenceNumber?: number; @ManyToOne({ entity: () => Group, primary: true }) - inGroup: Group; + inGroup!: Group; @ManyToOne({ entity: () => Student, diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 319061c5..e1ee8831 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -80,6 +80,7 @@ export async function createQuestion(questionDTO: QuestionDTO): Promise Date: Mon, 7 Apr 2025 00:44:20 +0200 Subject: [PATCH 03/71] fix(backend): Verdere door de aanpassingen veroorzaakte compilatiefouten opgelost --- backend/src/controllers/questions.ts | 15 +++++++++------ .../entities/assignments/submission.entity.ts | 2 +- backend/src/interfaces/group.ts | 18 ++++++++++++++++-- backend/src/interfaces/question.ts | 3 +++ backend/src/interfaces/submission.ts | 2 +- backend/src/routes/questions.ts | 6 +++--- backend/src/services/questions.ts | 15 +++++++++++++-- 7 files changed, 46 insertions(+), 15 deletions(-) diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 54b50fa9..0cfddd9b 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -4,6 +4,7 @@ import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { Language } from '@dwengo-1/common/util/language'; +import {getGroup} from "../services/groups"; function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { const { hruid, version } = req.params; @@ -21,16 +22,18 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu }; } -function getQuestionId(req: Request, res: Response): QuestionId | null { - const seq = req.params.seq; +async function getQuestionId(req: Request, res: Response): Promise { + const { seq, classId, assignmentId, groupId } = req.params const learningObjectIdentifier = getObjectId(req, res); + const groupIdentifier = await getGroup(classId, parseInt(assignmentId), parseInt(groupId), false); - if (!learningObjectIdentifier) { + if (!learningObjectIdentifier || !groupIdentifier) { return null; } return { learningObjectIdentifier, + inGroup: groupIdentifier, sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, }; } @@ -53,7 +56,7 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi } export async function getQuestionHandler(req: Request, res: Response): Promise { - const questionId = getQuestionId(req, res); + const questionId = await getQuestionId(req, res); if (!questionId) { return; @@ -69,7 +72,7 @@ export async function getQuestionHandler(req: Request, res: Response): Promise { - const questionId = getQuestionId(req, res); + const questionId = await getQuestionId(req, res); const full = req.query.full === 'true'; if (!questionId) { @@ -103,7 +106,7 @@ export async function createQuestionHandler(req: Request, res: Response): Promis } export async function deleteQuestionHandler(req: Request, res: Response): Promise { - const questionId = getQuestionId(req, res); + const questionId = await getQuestionId(req, res); if (!questionId) { return; diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index 1f500890..7a0ffc1c 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -25,7 +25,7 @@ export class Submission { entity: () => Group, primary: true }) - onBehalfOf: Group; + onBehalfOf!: Group; @ManyToOne({ entity: () => Student, diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts index 1a169b2b..da3ad845 100644 --- a/backend/src/interfaces/group.ts +++ b/backend/src/interfaces/group.ts @@ -1,7 +1,21 @@ import { Group } from '../entities/assignments/group.entity.js'; -import { mapToAssignmentDTO } from './assignment.js'; -import { mapToStudentDTO } from './student.js'; +import {mapToAssignment, mapToAssignmentDTO} from './assignment.js'; +import {mapToStudent, mapToStudentDTO} from './student.js'; import { GroupDTO } from '@dwengo-1/common/interfaces/group'; +import {getGroupRepository} from "../data/repositories"; +import {AssignmentDTO} from "@dwengo-1/common/interfaces/assignment"; +import {Class} from "../entities/classes/class.entity"; +import {StudentDTO} from "@dwengo-1/common/interfaces/student"; + +export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { + const assignmentDto = groupDto.assignment as AssignmentDTO; + + return getGroupRepository().create({ + groupNumber: groupDto.groupNumber, + assignment: mapToAssignment(assignmentDto, clazz!), + members: groupDto.members.map(studentDto => mapToStudent(studentDto as StudentDTO)) + }); +} export function mapToGroupDTO(group: Group): GroupDTO { return { diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 48d64f11..1b2b6fc9 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -2,6 +2,7 @@ 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'; +import {mapToGroupDTO} from "./group"; function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { return { @@ -21,6 +22,7 @@ export function mapToQuestionDTO(question: Question): QuestionDTO { learningObjectIdentifier, sequenceNumber: question.sequenceNumber!, author: mapToStudentDTO(question.author), + inGroup: mapToGroupDTO(question.inGroup), timestamp: question.timestamp.toISOString(), content: question.content, }; @@ -31,6 +33,7 @@ export function mapToQuestionDTOId(question: Question): QuestionId { return { learningObjectIdentifier, + inGroup: mapToGroupDTO(question.inGroup), sequenceNumber: question.sequenceNumber!, }; } diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts index b4ed4a2b..859370f6 100644 --- a/backend/src/interfaces/submission.ts +++ b/backend/src/interfaces/submission.ts @@ -14,7 +14,7 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { submissionNumber: submission.submissionNumber, submitter: mapToStudentDTO(submission.submitter), time: submission.submissionTime, - group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf) : undefined, + group: mapToGroupDTO(submission.onBehalfOf), content: submission.content, }; } diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index 31a71f3b..59e3fe58 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -15,11 +15,11 @@ router.get('/', getAllQuestionsHandler); router.post('/', createQuestionHandler); -router.delete('/:seq', deleteQuestionHandler); +router.delete('/:classId/assignment/:assignmentId/group/:groupId/:seq', deleteQuestionHandler); // Information about a question with id -router.get('/:seq', getQuestionHandler); +router.get('/:classId/assignment/:assignmentId/group/:groupId/:seq', getQuestionHandler); -router.get('/answers/:seq', getQuestionAnswersHandler); +router.get('/:classId/assignment/:assignmentId/group/:groupId/answers/:seq', getQuestionAnswersHandler); export default router; diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index e1ee8831..0bf5c734 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -1,4 +1,9 @@ -import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js'; +import { + getAnswerRepository, + getClassRepository, + getGroupRepository, + getQuestionRepository +} from '../data/repositories.js'; import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; import { Question } from '../entities/questions/question.entity.js'; import { Answer } from '../entities/questions/answer.entity.js'; @@ -8,6 +13,8 @@ import { LearningObjectIdentifier } from '../entities/content/learning-object-id import { mapToStudent } from '../interfaces/student.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; +import {AssignmentDTO} from "@dwengo-1/common/interfaces/assignment"; +import {mapToAssignment} from "../interfaces/assignment"; export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { const questionRepository: QuestionRepository = getQuestionRepository(); @@ -76,11 +83,15 @@ export async function createQuestion(questionDTO: QuestionDTO): Promise Date: Mon, 7 Apr 2025 14:50:15 +0200 Subject: [PATCH 04/71] fix(backend): group toch niet deel van primaire sleutel van vragen en submissions gemaakt, maar verplicht veld --- backend/src/controllers/questions.ts | 15 ++++++--------- .../src/entities/assignments/submission.entity.ts | 3 +-- backend/src/entities/questions/question.entity.ts | 2 +- backend/src/interfaces/question.ts | 1 - backend/src/interfaces/submission.ts | 1 - backend/src/routes/questions.ts | 6 +++--- backend/tests/setup-tests.ts | 2 +- .../assignments/submission.testdata.ts | 3 +++ .../test_assets/questions/questions.testdata.ts | 7 ++++++- common/src/interfaces/question.ts | 1 - 10 files changed, 21 insertions(+), 20 deletions(-) diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 0cfddd9b..54b50fa9 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -4,7 +4,6 @@ import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { Language } from '@dwengo-1/common/util/language'; -import {getGroup} from "../services/groups"; function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { const { hruid, version } = req.params; @@ -22,18 +21,16 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu }; } -async function getQuestionId(req: Request, res: Response): Promise { - const { seq, classId, assignmentId, groupId } = req.params +function getQuestionId(req: Request, res: Response): QuestionId | null { + const seq = req.params.seq; const learningObjectIdentifier = getObjectId(req, res); - const groupIdentifier = await getGroup(classId, parseInt(assignmentId), parseInt(groupId), false); - if (!learningObjectIdentifier || !groupIdentifier) { + if (!learningObjectIdentifier) { return null; } return { learningObjectIdentifier, - inGroup: groupIdentifier, sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, }; } @@ -56,7 +53,7 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi } export async function getQuestionHandler(req: Request, res: Response): Promise { - const questionId = await getQuestionId(req, res); + const questionId = getQuestionId(req, res); if (!questionId) { return; @@ -72,7 +69,7 @@ export async function getQuestionHandler(req: Request, res: Response): Promise { - const questionId = await getQuestionId(req, res); + const questionId = getQuestionId(req, res); const full = req.query.full === 'true'; if (!questionId) { @@ -106,7 +103,7 @@ export async function createQuestionHandler(req: Request, res: Response): Promis } export async function deleteQuestionHandler(req: Request, res: Response): Promise { - const questionId = await getQuestionId(req, res); + const questionId = getQuestionId(req, res); if (!questionId) { return; diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index 7a0ffc1c..728dd436 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -22,8 +22,7 @@ export class Submission { submissionNumber?: number; @ManyToOne({ - entity: () => Group, - primary: true + entity: () => Group }) onBehalfOf!: Group; diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index bb36e8a3..c6df4e6a 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -21,7 +21,7 @@ export class Question { @PrimaryKey({ type: 'integer', autoincrement: true }) sequenceNumber?: number; - @ManyToOne({ entity: () => Group, primary: true }) + @ManyToOne({ entity: () => Group }) inGroup!: Group; @ManyToOne({ diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 1b2b6fc9..91b90796 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -33,7 +33,6 @@ export function mapToQuestionDTOId(question: Question): QuestionId { return { learningObjectIdentifier, - inGroup: mapToGroupDTO(question.inGroup), sequenceNumber: question.sequenceNumber!, }; } diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts index 859370f6..bd80795a 100644 --- a/backend/src/interfaces/submission.ts +++ b/backend/src/interfaces/submission.ts @@ -38,7 +38,6 @@ export function mapToSubmission(submissionDTO: SubmissionDTO): Submission { submission.submitter = mapToStudent(submissionDTO.submitter); // Submission.submissionTime = submissionDTO.time; // Submission.onBehalfOf = submissionDTO.group!; - // TODO fix group submission.content = submissionDTO.content; return submission; diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index 59e3fe58..31a71f3b 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -15,11 +15,11 @@ router.get('/', getAllQuestionsHandler); router.post('/', createQuestionHandler); -router.delete('/:classId/assignment/:assignmentId/group/:groupId/:seq', deleteQuestionHandler); +router.delete('/:seq', deleteQuestionHandler); // Information about a question with id -router.get('/:classId/assignment/:assignmentId/group/:groupId/:seq', getQuestionHandler); +router.get('/:seq', getQuestionHandler); -router.get('/:classId/assignment/:assignmentId/group/:groupId/answers/:seq', getQuestionAnswersHandler); +router.get('/answers/:seq', getQuestionAnswersHandler); export default router; diff --git a/backend/tests/setup-tests.ts b/backend/tests/setup-tests.ts index 016099f3..5bd2fbd6 100644 --- a/backend/tests/setup-tests.ts +++ b/backend/tests/setup-tests.ts @@ -37,7 +37,7 @@ export async function setupTestApp(): Promise { learningObjects[1].attachments = attachments; - const questions = makeTestQuestions(em, students); + const questions = makeTestQuestions(em, students, groups); const answers = makeTestAnswers(em, teachers, questions); const submissions = makeTestSubmissions(em, students, groups); diff --git a/backend/tests/test_assets/assignments/submission.testdata.ts b/backend/tests/test_assets/assignments/submission.testdata.ts index f454b133..f6b49c6a 100644 --- a/backend/tests/test_assets/assignments/submission.testdata.ts +++ b/backend/tests/test_assets/assignments/submission.testdata.ts @@ -34,6 +34,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 1, submitter: students[0], submissionTime: new Date(2025, 2, 20), + onBehalfOf: groups[0], content: '', }); @@ -44,6 +45,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 2, submitter: students[0], submissionTime: new Date(2025, 2, 25), + onBehalfOf: groups[0], content: '', }); @@ -54,6 +56,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 1, submitter: students[1], submissionTime: new Date(2025, 2, 20), + onBehalfOf: groups[1], content: '', }); diff --git a/backend/tests/test_assets/questions/questions.testdata.ts b/backend/tests/test_assets/questions/questions.testdata.ts index dff742bb..0ccd498e 100644 --- a/backend/tests/test_assets/questions/questions.testdata.ts +++ b/backend/tests/test_assets/questions/questions.testdata.ts @@ -2,12 +2,14 @@ import { EntityManager } from '@mikro-orm/core'; import { Question } from '../../../src/entities/questions/question.entity'; import { Language } from '@dwengo-1/common/util/language'; import { Student } from '../../../src/entities/users/student.entity'; +import {Group} from "../../../src/entities/assignments/group.entity"; -export function makeTestQuestions(em: EntityManager, students: Student[]): Question[] { +export function makeTestQuestions(em: EntityManager, students: Student[], groups: Group[]): Question[] { const question01 = em.create(Question, { learningObjectLanguage: Language.English, learningObjectVersion: 1, learningObjectHruid: 'id05', + inGroup: groups[0], sequenceNumber: 1, author: students[0], timestamp: new Date(), @@ -18,6 +20,7 @@ export function makeTestQuestions(em: EntityManager, students: Student[]): Quest learningObjectLanguage: Language.English, learningObjectVersion: 1, learningObjectHruid: 'id05', + inGroup: groups[0], sequenceNumber: 2, author: students[2], timestamp: new Date(), @@ -30,6 +33,7 @@ export function makeTestQuestions(em: EntityManager, students: Student[]): Quest learningObjectHruid: 'id04', sequenceNumber: 1, author: students[0], + inGroup: groups[0], timestamp: new Date(), content: 'question', }); @@ -40,6 +44,7 @@ export function makeTestQuestions(em: EntityManager, students: Student[]): Quest learningObjectHruid: 'id01', sequenceNumber: 1, author: students[1], + inGroup: groups[1], timestamp: new Date(), content: 'question', }); diff --git a/common/src/interfaces/question.ts b/common/src/interfaces/question.ts index 69a9590d..9b00e1bd 100644 --- a/common/src/interfaces/question.ts +++ b/common/src/interfaces/question.ts @@ -14,5 +14,4 @@ export interface QuestionDTO { export interface QuestionId { learningObjectIdentifier: LearningObjectIdentifier; sequenceNumber: number; - inGroup: GroupDTO; } From 03fa7c7b14f5521f3b8e06ff224ab855bcadf697 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 7 Apr 2025 15:59:07 +0200 Subject: [PATCH 05/71] fix(backend): Fouten in de testen resulterend uit de aanpassingen opgelost. --- .../data/assignments/submission-repository.ts | 14 ++++- .../entities/assignments/assignment.entity.ts | 4 +- .../tests/data/questions/questions.test.ts | 12 +++- .../database-learning-path-provider.test.ts | 63 ++++++++++++++++--- 4 files changed, 80 insertions(+), 13 deletions(-) diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index f5090adc..f5ace213 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -42,11 +42,21 @@ export class SubmissionRepository extends DwengoEntityRepository { } public async findAllSubmissionsForGroup(group: Group): Promise { - return this.find({ onBehalfOf: group }); + return this.find( + { onBehalfOf: group }, + { + populate: ["onBehalfOf.members"] + } + ); } public async findAllSubmissionsForStudent(student: Student): Promise { - return this.find({ submitter: student }); + return this.find( + { submitter: student }, + { + populate: ["onBehalfOf.members"] + } + ); } public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { diff --git a/backend/src/entities/assignments/assignment.entity.ts b/backend/src/entities/assignments/assignment.entity.ts index 36b24344..52773909 100644 --- a/backend/src/entities/assignments/assignment.entity.ts +++ b/backend/src/entities/assignments/assignment.entity.ts @@ -1,4 +1,4 @@ -import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; +import {Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property} from '@mikro-orm/core'; import { Class } from '../classes/class.entity.js'; import { Group } from './group.entity.js'; import { Language } from '@dwengo-1/common/util/language'; @@ -35,5 +35,5 @@ export class Assignment { entity: () => Group, mappedBy: 'assignment', }) - groups!: Group[]; + groups!: Collection; } diff --git a/backend/tests/data/questions/questions.test.ts b/backend/tests/data/questions/questions.test.ts index 055a9d79..567777f5 100644 --- a/backend/tests/data/questions/questions.test.ts +++ b/backend/tests/data/questions/questions.test.ts @@ -1,7 +1,12 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { setupTestApp } from '../../setup-tests'; import { QuestionRepository } from '../../../src/data/questions/question-repository'; -import { getQuestionRepository, getStudentRepository } from '../../../src/data/repositories'; +import { + getAssignmentRepository, getClassRepository, + getGroupRepository, + getQuestionRepository, + getStudentRepository +} from '../../../src/data/repositories'; import { StudentRepository } from '../../../src/data/users/student-repository'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; import { Language } from '@dwengo-1/common/util/language'; @@ -27,8 +32,13 @@ describe('QuestionRepository', () => { it('should create new question', async () => { const id = new LearningObjectIdentifier('id03', Language.English, 1); const student = await studentRepository.findByUsername('Noordkaap'); + + const clazz = await getClassRepository().findById("id01"); + const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 1); + const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 1); await questionRepository.createQuestion({ loId: id, + inGroup: group!, author: student!, content: 'question?', }); diff --git a/backend/tests/services/learning-path/database-learning-path-provider.test.ts b/backend/tests/services/learning-path/database-learning-path-provider.test.ts index b8a733e7..2c7ceb0b 100644 --- a/backend/tests/services/learning-path/database-learning-path-provider.test.ts +++ b/backend/tests/services/learning-path/database-learning-path-provider.test.ts @@ -3,6 +3,9 @@ import { LearningObject } from '../../../src/entities/content/learning-object.en import { setupTestApp } from '../../setup-tests.js'; import { LearningPath } from '../../../src/entities/content/learning-path.entity.js'; import { + getAssignmentRepository, + getClassRepository, + getGroupRepository, getLearningObjectRepository, getLearningPathRepository, getStudentRepository, @@ -22,6 +25,10 @@ import { Student } from '../../../src/entities/users/student.entity.js'; import { LearningObjectNode, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; +const STUDENT_A_USERNAME = "student_a"; +const STUDENT_B_USERNAME = "student_b"; +const CLASS_NAME = "test_class" + async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { const learningObjectRepo = getLearningObjectRepository(); const learningPathRepo = getLearningPathRepository(); @@ -37,7 +44,10 @@ async function initPersonalizationTestData(): Promise<{ studentA: Student; studentB: Student; }> { - const studentRepo = getStudentRepository(); + const studentRepo = getStudentRepository() + const classRepo = getClassRepository(); + const assignmentRepo = getAssignmentRepository(); + const groupRepo = getGroupRepository(); const submissionRepo = getSubmissionRepository(); const learningPathRepo = getLearningPathRepository(); const learningObjectRepo = getLearningObjectRepository(); @@ -47,32 +57,69 @@ async function initPersonalizationTestData(): Promise<{ await learningObjectRepo.save(learningContent.extraExerciseObject); await learningPathRepo.save(learningContent.learningPath); + // Create students const studentA = studentRepo.create({ - username: 'student_a', + username: STUDENT_A_USERNAME, firstName: 'Aron', lastName: 'Student', }); await studentRepo.save(studentA); + + const studentB = studentRepo.create({ + username: STUDENT_B_USERNAME, + firstName: 'Bill', + lastName: 'Student', + }); + await studentRepo.save(studentB); + + // Create class for students + const testClass = classRepo.create({ + classId: CLASS_NAME, + displayName: "Test class" + }); + await classRepo.save(testClass); + + // Create assignment for students and assign them to different groups + const assignment = assignmentRepo.create({ + id: 0, + title: "Test assignment", + description: "Test description", + learningPathHruid: learningContent.learningPath.hruid, + learningPathLanguage: learningContent.learningPath.language, + within: testClass + }) + + const groupA = groupRepo.create({ + groupNumber: 0, + members: [studentA], + assignment + }); + await groupRepo.save(groupA); + + const groupB = groupRepo.create({ + groupNumber: 1, + members: [studentB], + assignment + }); + await groupRepo.save(groupB); + + // Let each of the students make a submission in his own group. const submissionA = submissionRepo.create({ learningObjectHruid: learningContent.branchingObject.hruid, learningObjectLanguage: learningContent.branchingObject.language, learningObjectVersion: learningContent.branchingObject.version, + onBehalfOf: groupA, submitter: studentA, submissionTime: new Date(), content: '[0]', }); await submissionRepo.save(submissionA); - const studentB = studentRepo.create({ - username: 'student_b', - firstName: 'Bill', - lastName: 'Student', - }); - await studentRepo.save(studentB); const submissionB = submissionRepo.create({ learningObjectHruid: learningContent.branchingObject.hruid, learningObjectLanguage: learningContent.branchingObject.language, learningObjectVersion: learningContent.branchingObject.version, + onBehalfOf: groupA, submitter: studentB, submissionTime: new Date(), content: '[1]', From 9135b9c5b02f918fe131bd2023a3b7c338947dd0 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 7 Apr 2025 19:23:46 +0200 Subject: [PATCH 06/71] feat(backend): Functionaliteit toegevoegd om alle submissions zichtbaar voor een bepaalde leerling of leerkracht op te vragen. --- .../data/assignments/assignment-repository.ts | 13 + .../data/assignments/submission-repository.ts | 25 ++ .../src/entities/assignments/group.entity.ts | 4 +- .../data/assignments/assignments.test.ts | 7 + .../data/assignments/submissions.test.ts | 82 ++++- .../assignments/assignments.testdata.ts | 12 +- .../assignments/groups.testdata.ts | 8 +- .../assignments/submission.testdata.ts | 24 +- package-lock.json | 319 ++++++++++-------- 9 files changed, 338 insertions(+), 156 deletions(-) diff --git a/backend/src/data/assignments/assignment-repository.ts b/backend/src/data/assignments/assignment-repository.ts index 3de5031d..296e67fd 100644 --- a/backend/src/data/assignments/assignment-repository.ts +++ b/backend/src/data/assignments/assignment-repository.ts @@ -6,6 +6,19 @@ export class AssignmentRepository extends DwengoEntityRepository { public async findByClassAndId(within: Class, id: number): Promise { return this.findOne({ within: within, id: id }); } + public async findAllByResponsibleTeacher(teacherUsername: string): Promise { + return this.findAll({ + where: { + within: { + teachers: { + $some: { + username: teacherUsername + } + } + } + } + }); + } public async findAllAssignmentsInClass(within: Class): Promise { return this.findAll({ where: { within: within } }); } diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index f5ace213..0a14bb0d 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -3,6 +3,7 @@ import { Group } from '../../entities/assignments/group.entity.js'; import { Submission } from '../../entities/assignments/submission.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { Student } from '../../entities/users/student.entity.js'; +import {Assignment} from "../../entities/assignments/assignment.entity"; export class SubmissionRepository extends DwengoEntityRepository { public async findSubmissionByLearningObjectAndSubmissionNumber( @@ -50,6 +51,30 @@ export class SubmissionRepository extends DwengoEntityRepository { ); } + public async findAllSubmissionsForAllGroupsOfStudent(studentUsername: string): Promise { + return this.findAll({ + where: { + onBehalfOf: { + members: { + $some: { + username: studentUsername + } + }, + } + } + }); + } + + public async findAllSubmissionsForAssignment(assignment: Assignment): Promise { + return this.findAll({ + where: { + onBehalfOf: { + assignment + } + } + }); + } + public async findAllSubmissionsForStudent(student: Student): Promise { return this.find( { submitter: student }, diff --git a/backend/src/entities/assignments/group.entity.ts b/backend/src/entities/assignments/group.entity.ts index cfe21f7f..1a69ed4b 100644 --- a/backend/src/entities/assignments/group.entity.ts +++ b/backend/src/entities/assignments/group.entity.ts @@ -1,4 +1,4 @@ -import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; +import {Collection, Entity, ManyToMany, ManyToOne, PrimaryKey} from '@mikro-orm/core'; import { Assignment } from './assignment.entity.js'; import { Student } from '../users/student.entity.js'; import { GroupRepository } from '../../data/assignments/group-repository.js'; @@ -19,5 +19,5 @@ export class Group { @ManyToMany({ entity: () => Student, }) - members!: Student[]; + members!: Collection; } diff --git a/backend/tests/data/assignments/assignments.test.ts b/backend/tests/data/assignments/assignments.test.ts index c26fb5ba..f587e979 100644 --- a/backend/tests/data/assignments/assignments.test.ts +++ b/backend/tests/data/assignments/assignments.test.ts @@ -31,6 +31,13 @@ describe('AssignmentRepository', () => { expect(assignments[0].title).toBe('tool'); }); + it('should find all by username of the responsible teacher', async () => { + const result = await assignmentRepository.findAllByResponsibleTeacher("FooFighters") + const resultIds = result.map(it => it.id).sort(); + + expect(resultIds).toEqual([1, 3, 4]); + }); + it('should not find removed assignment', async () => { const class_ = await classRepository.findById('id01'); await assignmentRepository.deleteByClassAndId(class_!, 3); diff --git a/backend/tests/data/assignments/submissions.test.ts b/backend/tests/data/assignments/submissions.test.ts index 85e1bc11..9d72c963 100644 --- a/backend/tests/data/assignments/submissions.test.ts +++ b/backend/tests/data/assignments/submissions.test.ts @@ -1,6 +1,6 @@ -import { beforeAll, describe, expect, it } from 'vitest'; -import { setupTestApp } from '../../setup-tests'; -import { SubmissionRepository } from '../../../src/data/assignments/submission-repository'; +import {beforeAll, describe, expect, it} from 'vitest'; +import {setupTestApp} from '../../setup-tests'; +import {SubmissionRepository} from '../../../src/data/assignments/submission-repository'; import { getAssignmentRepository, getClassRepository, @@ -8,12 +8,32 @@ import { getStudentRepository, getSubmissionRepository, } from '../../../src/data/repositories'; -import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; -import { Language } from '@dwengo-1/common/util/language'; -import { StudentRepository } from '../../../src/data/users/student-repository'; -import { GroupRepository } from '../../../src/data/assignments/group-repository'; -import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository'; -import { ClassRepository } from '../../../src/data/classes/class-repository'; +import {LearningObjectIdentifier} from '../../../src/entities/content/learning-object-identifier'; +import {Language} from '@dwengo-1/common/util/language'; +import {StudentRepository} from '../../../src/data/users/student-repository'; +import {GroupRepository} from '../../../src/data/assignments/group-repository'; +import {AssignmentRepository} from '../../../src/data/assignments/assignment-repository'; +import {ClassRepository} from '../../../src/data/classes/class-repository'; +import {Submission} from "../../../src/entities/assignments/submission.entity"; + +export function checkSubmissionsForStudentNoordkaap(result: Submission[]) { + sortSubmissions(result); + + expect(result[0].learningObjectHruid).toBe("id01"); + expect(result[0].submissionNumber).toBe(2); + + expect(result[1].learningObjectHruid).toBe("id02"); + expect(result[1].submissionNumber).toBe(1); + + expect(result[2].learningObjectHruid).toBe("id02"); + expect(result[2].submissionNumber).toBe(2); + + expect(result[3].learningObjectHruid).toBe("id03"); + expect(result[3].submissionNumber).toBe(1); + + expect(result[4].learningObjectHruid).toBe("id03"); + expect(result[4].submissionNumber).toBe(2); +} describe('SubmissionRepository', () => { let submissionRepository: SubmissionRepository; @@ -59,6 +79,42 @@ describe('SubmissionRepository', () => { expect(submission?.submissionTime.getDate()).toBe(25); }); + it('should find all submissions for all groups of a student', async () => { + const result = await submissionRepository.findAllSubmissionsForAllGroupsOfStudent("Noordkaap"); + expect(result.length).toBe(5); + + checkSubmissionsForStudentNoordkaap(result); + }); + + it('should find all submissions for a certain assignment', async () => { + const clazz = await classRepository.findById('id01'); + const assignment = await assignmentRepository.findByClassAndId(clazz!, 1); + const result = await submissionRepository.findAllSubmissionsForAssignment(assignment!); + + sortSubmissions(result); + + expect(result).toHaveLength(5); + + expect(result[0].learningObjectHruid).toBe("id01"); + expect(result[0].submissionNumber).toBe(1); + + expect(result[1].learningObjectHruid).toBe("id02"); + expect(result[1].submissionNumber).toBe(1); + + expect(result[2].learningObjectHruid).toBe("id02"); + expect(result[2].submissionNumber).toBe(2); + + expect(result[3].learningObjectHruid).toBe("id03"); + expect(result[3].submissionNumber).toBe(1); + + expect(result[4].learningObjectHruid).toBe("id03"); + expect(result[4].submissionNumber).toBe(2); + + // But not submission7 (id01, submission number: 3), since it was submitted for an assignment + + sortSubmissions(result); + }); + it('should not find a deleted submission', async () => { const id = new LearningObjectIdentifier('id01', Language.English, 1); await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1); @@ -68,3 +124,11 @@ describe('SubmissionRepository', () => { expect(submission).toBeNull(); }); }); + +function sortSubmissions(submissions: Submission[]) { + submissions.sort((a, b) => { + if (a.learningObjectHruid < b.learningObjectHruid) return -1; + if (a.learningObjectHruid > b.learningObjectHruid) return 1; + return a.submissionNumber! - b.submissionNumber!; + }); +} diff --git a/backend/tests/test_assets/assignments/assignments.testdata.ts b/backend/tests/test_assets/assignments/assignments.testdata.ts index b0da638f..14253c0a 100644 --- a/backend/tests/test_assets/assignments/assignments.testdata.ts +++ b/backend/tests/test_assets/assignments/assignments.testdata.ts @@ -34,5 +34,15 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign groups: [], }); - return [assignment01, assignment02, assignment03]; + const assignment04 = em.create(Assignment, { + within: classes[0], + id: 4, + title: 'another assignment', + description: 'with a description', + learningPathHruid: 'id01', + learningPathLanguage: Language.English, + groups: [], + }); + + return [assignment01, assignment02, assignment03, assignment04]; } diff --git a/backend/tests/test_assets/assignments/groups.testdata.ts b/backend/tests/test_assets/assignments/groups.testdata.ts index a8ff8380..761f0736 100644 --- a/backend/tests/test_assets/assignments/groups.testdata.ts +++ b/backend/tests/test_assets/assignments/groups.testdata.ts @@ -28,5 +28,11 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen members: students.slice(3, 4), }); - return [group01, group02, group03, group04]; + const group05 = em.create(Group, { + assignment: assignments[3], + groupNumber: 1, + members: students.slice(0, 2), + }); + + return [group01, group02, group03, group04, group05]; } diff --git a/backend/tests/test_assets/assignments/submission.testdata.ts b/backend/tests/test_assets/assignments/submission.testdata.ts index f6b49c6a..da77539a 100644 --- a/backend/tests/test_assets/assignments/submission.testdata.ts +++ b/backend/tests/test_assets/assignments/submission.testdata.ts @@ -60,5 +60,27 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou content: '', }); - return [submission01, submission02, submission03, submission04, submission05]; + const submission06 = em.create(Submission, { + learningObjectHruid: 'id01', + learningObjectLanguage: Language.English, + learningObjectVersion: 1, + submissionNumber: 2, + submitter: students[1], + submissionTime: new Date(2025, 2, 25), + onBehalfOf: groups[4], + content: '', + }); + + const submission07 = em.create(Submission, { + learningObjectHruid: 'id01', + learningObjectLanguage: Language.English, + learningObjectVersion: 1, + submissionNumber: 3, + submitter: students[3], + submissionTime: new Date(2025, 3, 25), + onBehalfOf: groups[3], + content: '', + }); + + return [submission01, submission02, submission03, submission04, submission05, submission06, submission07]; } diff --git a/package-lock.json b/package-lock.json index 27d261cb..acf0fca0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,133 @@ "vitest": "^3.0.6" } }, + "backend/node_modules/@mikro-orm/cli": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.9.tgz", + "integrity": "sha512-LQzVsmar/0DoJkPGyz3OpB8pa9BCQtvYreEC71h0O+RcizppJjgBQNTkj5tJd2Iqvh4hSaMv6qTv0l5UK6F2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jercle/yargonaut": "1.1.5", + "@mikro-orm/core": "6.4.9", + "@mikro-orm/knex": "6.4.9", + "fs-extra": "11.3.0", + "tsconfig-paths": "4.2.0", + "yargs": "17.7.2" + }, + "bin": { + "mikro-orm": "cli", + "mikro-orm-esm": "esm" + }, + "engines": { + "node": ">= 18.12.0" + } + }, + "backend/node_modules/@mikro-orm/core": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.9.tgz", + "integrity": "sha512-osB2TbvSH4ZL1s62LCBQFAnxPqLycX5fakPHOoztudixqfbVD5QQydeGizJXMMh2zKP6vRCwIJy3MeSuFxPjHg==", + "license": "MIT", + "dependencies": { + "dataloader": "2.2.3", + "dotenv": "16.4.7", + "esprima": "4.0.1", + "fs-extra": "11.3.0", + "globby": "11.1.0", + "mikro-orm": "6.4.9", + "reflect-metadata": "0.2.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/b4nan" + } + }, + "backend/node_modules/@mikro-orm/knex": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.9.tgz", + "integrity": "sha512-iGXJfe/TziVOQsWuxMIqkOpurysWzQA6kj3+FDtOkHJAijZhqhjSBnfUVHHY/JzU9o0M0rgLrDVJFry/uEaJEA==", + "license": "MIT", + "dependencies": { + "fs-extra": "11.3.0", + "knex": "3.1.0", + "sqlstring": "2.3.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0", + "better-sqlite3": "*", + "libsql": "*", + "mariadb": "*" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "libsql": { + "optional": true + }, + "mariadb": { + "optional": true + } + } + }, + "backend/node_modules/@mikro-orm/postgresql": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/postgresql/-/postgresql-6.4.9.tgz", + "integrity": "sha512-ZdVVFAL/TSbzpEmChGdH0oUpy2KiHLjNIeItZHRQgInn1X9p0qx28VVDR78p8qgRGkQ3LquxGTkvmWI0w7qi3A==", + "license": "MIT", + "dependencies": { + "@mikro-orm/knex": "6.4.9", + "pg": "8.13.3", + "postgres-array": "3.0.4", + "postgres-date": "2.1.0", + "postgres-interval": "4.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "backend/node_modules/@mikro-orm/reflection": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/reflection/-/reflection-6.4.9.tgz", + "integrity": "sha512-fgY7yLrcZm3J/8dv9reUC4PQo7C2muImU31jmzz1SxmNKPJFDJl7OzcDZlM5NOisXzsWUBrcNdCyuQiWViVc3A==", + "license": "MIT", + "dependencies": { + "globby": "11.1.0", + "ts-morph": "25.0.1" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "backend/node_modules/@mikro-orm/sqlite": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/sqlite/-/sqlite-6.4.9.tgz", + "integrity": "sha512-O7Jy/5DrTWpJI/3qkhRJHl+OcECx1N625LHDODAAauOK3+MJB/bj80TrvQhe6d/CHZMmvxZ7m2GzaL1NulKxRw==", + "license": "MIT", + "dependencies": { + "@mikro-orm/knex": "6.4.9", + "fs-extra": "11.3.0", + "sqlite3": "5.1.7", + "sqlstring-sqlite": "0.1.1" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, "backend/node_modules/globals": { "version": "15.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", @@ -85,6 +212,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "backend/node_modules/mikro-orm": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.9.tgz", + "integrity": "sha512-XwVrWNT4NNwS6kHIKFNDfvy8L1eWcBBEHeTVzFFYcnb2ummATaLxqeVkNEmKA68jmdtfQdUmWBqGdbcIPwtL2Q==", + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + } + }, + "backend/node_modules/pg": { + "version": "8.13.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz", + "integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.1", + "pg-protocol": "^1.7.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "backend/node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", + "license": "MIT" + }, "common": { "name": "@dwengo-1/common", "version": "0.1.1" @@ -1816,133 +1985,6 @@ "jsep": "^0.4.0||^1.0.0" } }, - "node_modules/@mikro-orm/cli": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.9.tgz", - "integrity": "sha512-LQzVsmar/0DoJkPGyz3OpB8pa9BCQtvYreEC71h0O+RcizppJjgBQNTkj5tJd2Iqvh4hSaMv6qTv0l5UK6F2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jercle/yargonaut": "1.1.5", - "@mikro-orm/core": "6.4.9", - "@mikro-orm/knex": "6.4.9", - "fs-extra": "11.3.0", - "tsconfig-paths": "4.2.0", - "yargs": "17.7.2" - }, - "bin": { - "mikro-orm": "cli", - "mikro-orm-esm": "esm" - }, - "engines": { - "node": ">= 18.12.0" - } - }, - "node_modules/@mikro-orm/core": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.9.tgz", - "integrity": "sha512-osB2TbvSH4ZL1s62LCBQFAnxPqLycX5fakPHOoztudixqfbVD5QQydeGizJXMMh2zKP6vRCwIJy3MeSuFxPjHg==", - "license": "MIT", - "dependencies": { - "dataloader": "2.2.3", - "dotenv": "16.4.7", - "esprima": "4.0.1", - "fs-extra": "11.3.0", - "globby": "11.1.0", - "mikro-orm": "6.4.9", - "reflect-metadata": "0.2.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/b4nan" - } - }, - "node_modules/@mikro-orm/knex": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.9.tgz", - "integrity": "sha512-iGXJfe/TziVOQsWuxMIqkOpurysWzQA6kj3+FDtOkHJAijZhqhjSBnfUVHHY/JzU9o0M0rgLrDVJFry/uEaJEA==", - "license": "MIT", - "dependencies": { - "fs-extra": "11.3.0", - "knex": "3.1.0", - "sqlstring": "2.3.3" - }, - "engines": { - "node": ">= 18.12.0" - }, - "peerDependencies": { - "@mikro-orm/core": "^6.0.0", - "better-sqlite3": "*", - "libsql": "*", - "mariadb": "*" - }, - "peerDependenciesMeta": { - "better-sqlite3": { - "optional": true - }, - "libsql": { - "optional": true - }, - "mariadb": { - "optional": true - } - } - }, - "node_modules/@mikro-orm/postgresql": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mikro-orm/postgresql/-/postgresql-6.4.9.tgz", - "integrity": "sha512-ZdVVFAL/TSbzpEmChGdH0oUpy2KiHLjNIeItZHRQgInn1X9p0qx28VVDR78p8qgRGkQ3LquxGTkvmWI0w7qi3A==", - "license": "MIT", - "dependencies": { - "@mikro-orm/knex": "6.4.9", - "pg": "8.13.3", - "postgres-array": "3.0.4", - "postgres-date": "2.1.0", - "postgres-interval": "4.0.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "peerDependencies": { - "@mikro-orm/core": "^6.0.0" - } - }, - "node_modules/@mikro-orm/reflection": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mikro-orm/reflection/-/reflection-6.4.9.tgz", - "integrity": "sha512-fgY7yLrcZm3J/8dv9reUC4PQo7C2muImU31jmzz1SxmNKPJFDJl7OzcDZlM5NOisXzsWUBrcNdCyuQiWViVc3A==", - "license": "MIT", - "dependencies": { - "globby": "11.1.0", - "ts-morph": "25.0.1" - }, - "engines": { - "node": ">= 18.12.0" - }, - "peerDependencies": { - "@mikro-orm/core": "^6.0.0" - } - }, - "node_modules/@mikro-orm/sqlite": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mikro-orm/sqlite/-/sqlite-6.4.9.tgz", - "integrity": "sha512-O7Jy/5DrTWpJI/3qkhRJHl+OcECx1N625LHDODAAauOK3+MJB/bj80TrvQhe6d/CHZMmvxZ7m2GzaL1NulKxRw==", - "license": "MIT", - "dependencies": { - "@mikro-orm/knex": "6.4.9", - "fs-extra": "11.3.0", - "sqlite3": "5.1.7", - "sqlstring-sqlite": "0.1.1" - }, - "engines": { - "node": ">= 18.12.0" - }, - "peerDependencies": { - "@mikro-orm/core": "^6.0.0" - } - }, "node_modules/@napi-rs/snappy-android-arm-eabi": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.2.2.tgz", @@ -7849,15 +7891,6 @@ "node": ">=8.6" } }, - "node_modules/mikro-orm": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.9.tgz", - "integrity": "sha512-XwVrWNT4NNwS6kHIKFNDfvy8L1eWcBBEHeTVzFFYcnb2ummATaLxqeVkNEmKA68jmdtfQdUmWBqGdbcIPwtL2Q==", - "license": "MIT", - "engines": { - "node": ">= 18.12.0" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -8649,14 +8682,15 @@ "license": "MIT" }, "node_modules/pg": { - "version": "8.13.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz", - "integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", + "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.7.0", - "pg-pool": "^3.7.1", - "pg-protocol": "^1.7.1", + "pg-pool": "^3.8.0", + "pg-protocol": "^1.8.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -8762,7 +8796,8 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/pgpass": { "version": "1.0.5", From 64fd66a1deeb87b3cc40f8d6f3273cf6453fe23c Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 7 Apr 2025 21:48:28 +0200 Subject: [PATCH 07/71] feat(backend): Submissions kunnen nu per leerobject, assignment en optioneel groepslid opgevraagd worden --- .../data/assignments/submission-repository.ts | 36 +++++++----- .../data/assignments/submissions.test.ts | 58 +++++++++++-------- .../assignments/groups.testdata.ts | 20 +++++++ .../assignments/submission.testdata.ts | 27 ++++++--- 4 files changed, 94 insertions(+), 47 deletions(-) diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index 0a14bb0d..00cdac14 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -51,26 +51,32 @@ export class SubmissionRepository extends DwengoEntityRepository { ); } - public async findAllSubmissionsForAllGroupsOfStudent(studentUsername: string): Promise { - return this.findAll({ - where: { - onBehalfOf: { - members: { - $some: { - username: studentUsername - } - }, + /** + * Looks up all submissions for the given learning object which were submitted as part of the given assignment. + * When forStudentUsername is set, only the submissions of the given user's group are shown. + */ + public async findAllSubmissionsForLearningObjectAndAssignment( + loId: LearningObjectIdentifier, + assignment: Assignment, + forStudentUsername?: string + ): Promise { + let onBehalfOf = forStudentUsername ? { + assignment, + members: { + $some: { + username: forStudentUsername } } - }); - } + } : { + assignment + }; - public async findAllSubmissionsForAssignment(assignment: Assignment): Promise { return this.findAll({ where: { - onBehalfOf: { - assignment - } + learningObjectHruid: loId.hruid, + learningObjectLanguage: loId.language, + learningObjectVersion: loId.version, + onBehalfOf } }); } diff --git a/backend/tests/data/assignments/submissions.test.ts b/backend/tests/data/assignments/submissions.test.ts index 9d72c963..a723e86f 100644 --- a/backend/tests/data/assignments/submissions.test.ts +++ b/backend/tests/data/assignments/submissions.test.ts @@ -15,6 +15,8 @@ import {GroupRepository} from '../../../src/data/assignments/group-repository'; import {AssignmentRepository} from '../../../src/data/assignments/assignment-repository'; import {ClassRepository} from '../../../src/data/classes/class-repository'; import {Submission} from "../../../src/entities/assignments/submission.entity"; +import {Class} from "../../../src/entities/classes/class.entity"; +import {Assignment} from "../../../src/entities/assignments/assignment.entity"; export function checkSubmissionsForStudentNoordkaap(result: Submission[]) { sortSubmissions(result); @@ -79,40 +81,48 @@ describe('SubmissionRepository', () => { expect(submission?.submissionTime.getDate()).toBe(25); }); - it('should find all submissions for all groups of a student', async () => { - const result = await submissionRepository.findAllSubmissionsForAllGroupsOfStudent("Noordkaap"); - expect(result.length).toBe(5); - - checkSubmissionsForStudentNoordkaap(result); - }); - - it('should find all submissions for a certain assignment', async () => { - const clazz = await classRepository.findById('id01'); - const assignment = await assignmentRepository.findByClassAndId(clazz!, 1); - const result = await submissionRepository.findAllSubmissionsForAssignment(assignment!); - + let clazz: Class | null; + let assignment: Assignment | null; + let loId: LearningObjectIdentifier; + it('should find all submissions for a certain learning object and assignment', async () => { + clazz = await classRepository.findById('id01'); + assignment = await assignmentRepository.findByClassAndId(clazz!, 1); + loId = { + hruid: "id02", + language: Language.English, + version: 1 + }; + const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!); sortSubmissions(result); - expect(result).toHaveLength(5); + expect(result).toHaveLength(3); - expect(result[0].learningObjectHruid).toBe("id01"); + // submission3 should be found (for learning object 'id02' by group #1 for Assignment #1 in class 'id01') + expect(result[0].learningObjectHruid).toBe(loId.hruid); expect(result[0].submissionNumber).toBe(1); - expect(result[1].learningObjectHruid).toBe("id02"); - expect(result[1].submissionNumber).toBe(1); + // submission4 should be found (for learning object 'id02' by group #1 for Assignment #1 in class 'id01') + expect(result[1].learningObjectHruid).toBe(loId.hruid); + expect(result[1].submissionNumber).toBe(2); - expect(result[2].learningObjectHruid).toBe("id02"); - expect(result[2].submissionNumber).toBe(2); + // submission8 should be found (for learning object 'id02' by group #2 for Assignment #1 in class 'id01') + expect(result[2].learningObjectHruid).toBe(loId.hruid); + expect(result[2].submissionNumber).toBe(3); + }); - expect(result[3].learningObjectHruid).toBe("id03"); - expect(result[3].submissionNumber).toBe(1); + it("should find only the submissions for a certain learning object and assignment made for the user's group", async () => { + const result = + await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, "Tool"); + // (student Tool is in group #2) - expect(result[4].learningObjectHruid).toBe("id03"); - expect(result[4].submissionNumber).toBe(2); + expect(result).toHaveLength(1); - // But not submission7 (id01, submission number: 3), since it was submitted for an assignment + // submission8 should be found (for learning object 'id02' by group #2 for Assignment #1 in class 'id01') + expect(result[0].learningObjectHruid).toBe(loId.hruid); + expect(result[0].submissionNumber).toBe(3); - sortSubmissions(result); + // The other submissions found in the previous test case should not be found anymore as they were made on + // behalf of group #1 which Tool is no member of. }); it('should not find a deleted submission', async () => { diff --git a/backend/tests/test_assets/assignments/groups.testdata.ts b/backend/tests/test_assets/assignments/groups.testdata.ts index 761f0736..c82887bb 100644 --- a/backend/tests/test_assets/assignments/groups.testdata.ts +++ b/backend/tests/test_assets/assignments/groups.testdata.ts @@ -4,30 +4,50 @@ import { Assignment } from '../../../src/entities/assignments/assignment.entity' import { Student } from '../../../src/entities/users/student.entity'; export function makeTestGroups(em: EntityManager, students: Student[], assignments: Assignment[]): Group[] { + /* + * Group #1 for Assignment #1 in class 'id01' + * => Assigned to do learning path 'id02' + */ const group01 = em.create(Group, { assignment: assignments[0], groupNumber: 1, members: students.slice(0, 2), }); + /* + * Group #2 for Assignment #1 in class 'id01' + * => Assigned to do learning path 'id02' + */ const group02 = em.create(Group, { assignment: assignments[0], groupNumber: 2, members: students.slice(2, 4), }); + /* + * Group #3 for Assignment #1 in class 'id01' + * => Assigned to do learning path 'id02' + */ const group03 = em.create(Group, { assignment: assignments[0], groupNumber: 3, members: students.slice(4, 6), }); + /* + * Group #4 for Assignment #2 in class 'id02' + * => Assigned to do learning path 'id01' + */ const group04 = em.create(Group, { assignment: assignments[1], groupNumber: 4, members: students.slice(3, 4), }); + /* + * Group #5 for Assignment #4 in class 'id01' + * => Assigned to do learning path 'id01' + */ const group05 = em.create(Group, { assignment: assignments[3], groupNumber: 1, diff --git a/backend/tests/test_assets/assignments/submission.testdata.ts b/backend/tests/test_assets/assignments/submission.testdata.ts index da77539a..812d1289 100644 --- a/backend/tests/test_assets/assignments/submission.testdata.ts +++ b/backend/tests/test_assets/assignments/submission.testdata.ts @@ -12,7 +12,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 1, submitter: students[0], submissionTime: new Date(2025, 2, 20), - onBehalfOf: groups[0], + onBehalfOf: groups[0], // group #1 for Assignment #1 in class 'id01' content: 'sub1', }); @@ -23,7 +23,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 2, submitter: students[0], submissionTime: new Date(2025, 2, 25), - onBehalfOf: groups[0], + onBehalfOf: groups[0], // group #1 for Assignment #1 in class 'id01' content: '', }); @@ -34,7 +34,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 1, submitter: students[0], submissionTime: new Date(2025, 2, 20), - onBehalfOf: groups[0], + onBehalfOf: groups[0], // group #1 for Assignment #1 in class 'id01' content: '', }); @@ -45,7 +45,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 2, submitter: students[0], submissionTime: new Date(2025, 2, 25), - onBehalfOf: groups[0], + onBehalfOf: groups[0], // group #1 for Assignment #1 in class 'id01' content: '', }); @@ -56,7 +56,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 1, submitter: students[1], submissionTime: new Date(2025, 2, 20), - onBehalfOf: groups[1], + onBehalfOf: groups[1], // Group #2 for Assignment #1 in class 'id01' content: '', }); @@ -67,7 +67,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 2, submitter: students[1], submissionTime: new Date(2025, 2, 25), - onBehalfOf: groups[4], + onBehalfOf: groups[4], // Group #5 for Assignment #4 in class 'id01' content: '', }); @@ -78,9 +78,20 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 3, submitter: students[3], submissionTime: new Date(2025, 3, 25), - onBehalfOf: groups[3], + onBehalfOf: groups[3], // Group #4 for Assignment #2 in class 'id02' content: '', }); - return [submission01, submission02, submission03, submission04, submission05, submission06, submission07]; + const submission08 = em.create(Submission, { + learningObjectHruid: 'id02', + learningObjectLanguage: Language.English, + learningObjectVersion: 1, + submissionNumber: 3, + submitter: students[1], + submissionTime: new Date(2025, 4, 7), + onBehalfOf: groups[1], // Group #2 for Assignment #1 in class 'id01' + content: '', + }); + + return [submission01, submission02, submission03, submission04, submission05, submission06, submission07, submission08]; } From c863dc627ff1fec33152aff8450e2c5882746fef Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 8 Apr 2025 00:26:59 +0200 Subject: [PATCH 08/71] feat(backend): Vragen kunnen nu per leerobject, assignment en optioneel groepslid opgevraagd worden --- backend/src/controllers/questions.ts | 74 ++++++++++++++++--- backend/src/controllers/submissions.ts | 34 ++++++++- .../data/assignments/assignment-repository.ts | 3 + .../src/data/questions/question-repository.ts | 31 ++++++++ backend/src/routes/submissions.ts | 13 ++-- backend/src/services/questions.ts | 25 ++++++- backend/src/services/submissions.ts | 23 +++++- 7 files changed, 181 insertions(+), 22 deletions(-) diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 54b50fa9..da1d020e 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -1,11 +1,28 @@ import { Request, Response } from 'express'; -import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; +import { + createQuestion, + deleteQuestion, + getAllQuestions, + getAnswersByQuestion, + getQuestion, + getQuestionsAboutLearningObjectInAssignment +} from '../services/questions.js'; import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { Language } from '@dwengo-1/common/util/language'; +import {AnswerDTO, AnswerId} from "@dwengo-1/common/interfaces/answer"; -function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { +interface QuestionPathParams { + hruid: string; + version: string; +} + +interface QuestionQueryParams { + lang: string; +} + +function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { const { hruid, version } = req.params; const lang = req.query.lang; @@ -21,7 +38,10 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu }; } -function getQuestionId(req: Request, res: Response): QuestionId | null { +interface GetQuestionIdPathParams extends QuestionPathParams { + seq: string; +} +function getQuestionId(req: Request, res: Response): QuestionId | null { const seq = req.params.seq; const learningObjectIdentifier = getObjectId(req, res); @@ -35,15 +55,35 @@ function getQuestionId(req: Request, res: Response): QuestionId | null { }; } -export async function getAllQuestionsHandler(req: Request, res: Response): Promise { +interface GetAllQuestionsQueryParams extends QuestionQueryParams { + classId?: string, + assignmentId?: number, + forStudent?: string, + full?: boolean +} + +export async function getAllQuestionsHandler( + req: Request, + res: Response +): Promise { const objectId = getObjectId(req, res); - const full = req.query.full === 'true'; + const full = req.query.full; if (!objectId) { return; } - - const questions = await getAllQuestions(objectId, full); + let questions: QuestionDTO[] | QuestionId[]; + if (req.query.classId && req.query.assignmentId) { + questions = await getQuestionsAboutLearningObjectInAssignment( + objectId, + req.query.classId, + req.query.assignmentId, + full ?? false, + req.query.forStudent + ); + } else { + questions = await getAllQuestions(objectId, full ?? false); + } if (!questions) { res.status(404).json({ error: `Questions not found.` }); @@ -52,7 +92,10 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi } } -export async function getQuestionHandler(req: Request, res: Response): Promise { +export async function getQuestionHandler( + req: Request, + res: Response +): Promise { const questionId = getQuestionId(req, res); if (!questionId) { @@ -68,9 +111,15 @@ export async function getQuestionHandler(req: Request, res: Response): Promise { +interface GetQuestionAnswersQueryParams extends QuestionQueryParams { + full: boolean +} +export async function getQuestionAnswersHandler( + req: Request, + res: Response +): Promise { const questionId = getQuestionId(req, res); - const full = req.query.full === 'true'; + const full = req.query.full; if (!questionId) { return; @@ -102,7 +151,10 @@ export async function createQuestionHandler(req: Request, res: Response): Promis } } -export async function deleteQuestionHandler(req: Request, res: Response): Promise { +export async function deleteQuestionHandler( + req: Request, + res: Response +): Promise { const questionId = getQuestionId(req, res); if (!questionId) { diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index 239eb6d7..3a5aa391 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -1,13 +1,45 @@ import { Request, Response } from 'express'; -import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js'; +import { + createSubmission, + deleteSubmission, + getSubmission, + getSubmissionsForLearningObjectAndAssignment +} from '../services/submissions.js'; import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; import { Language, languageMap } from '@dwengo-1/common/util/language'; +import {Submission} from "../entities/assignments/submission.entity"; interface SubmissionParams { hruid: string; id: number; } +interface SubmissionQuery { + language: string, + version: number; +} + +interface SubmissionsQuery extends SubmissionQuery { + classId: string, + assignmentId: number, + studentUsername?: string +} + +export async function getSubmissionsHandler( + req: Request, + res: Response +): Promise { + const loHruid = req.params.hruid; + const lang = languageMap[req.query.language as string] || Language.Dutch; + const version = (req.query.version || 1) as number; + + const submissions = await getSubmissionsForLearningObjectAndAssignment( + loHruid, lang, version, req.query.classId, req.query.assignmentId + ); + + res.json(submissions); +} + export async function getSubmissionHandler(req: Request, res: Response): Promise { const lohruid = req.params.hruid; const submissionNumber = Number(req.params.id); diff --git a/backend/src/data/assignments/assignment-repository.ts b/backend/src/data/assignments/assignment-repository.ts index 296e67fd..c6766af9 100644 --- a/backend/src/data/assignments/assignment-repository.ts +++ b/backend/src/data/assignments/assignment-repository.ts @@ -6,6 +6,9 @@ export class AssignmentRepository extends DwengoEntityRepository { public async findByClassAndId(within: Class, id: number): Promise { return this.findOne({ within: within, id: id }); } + public async findByClassIdAndAssignmentId(withinClass: string, id: number): Promise { + return this.findOne({ within: { classId: withinClass }, id: id }); + } public async findAllByResponsibleTeacher(teacherUsername: string): Promise { return this.findAll({ where: { diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 51df5afe..f36b1074 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -4,6 +4,7 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object import { Student } from '../../entities/users/student.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; import {Group} from "../../entities/assignments/group.entity"; +import {Assignment} from "../../entities/assignments/assignment.entity"; export class QuestionRepository extends DwengoEntityRepository { public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group, content: string }): Promise { @@ -64,4 +65,34 @@ export class QuestionRepository extends DwengoEntityRepository { orderBy: { timestamp: 'DESC' }, // New to old }); } + + /** + * Looks up all questions for the given learning object which were asked as part of the given assignment. + * When forStudentUsername is set, only the questions within the given user's group are shown. + */ + public async findAllQuestionsAboutLearningObjectInAssignment( + loId: LearningObjectIdentifier, + assignment: Assignment, + forStudentUsername?: string + ): Promise { + let inGroup = forStudentUsername ? { + assignment, + members: { + $some: { + username: forStudentUsername + } + } + } : { + assignment + }; + + return this.findAll({ + where: { + learningObjectHruid: loId.hruid, + learningObjectLanguage: loId.language, + learningObjectVersion: loId.version, + inGroup + } + }); + } } diff --git a/backend/src/routes/submissions.ts b/backend/src/routes/submissions.ts index 8e9831b9..930e1a59 100644 --- a/backend/src/routes/submissions.ts +++ b/backend/src/routes/submissions.ts @@ -1,13 +1,14 @@ import express from 'express'; -import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler } from '../controllers/submissions.js'; +import { + createSubmissionHandler, + deleteSubmissionHandler, + getSubmissionHandler, + getSubmissionsHandler +} from '../controllers/submissions.js'; const router = express.Router({ mergeParams: true }); // Root endpoint used to search objects -router.get('/', (_req, res) => { - res.json({ - submissions: ['0', '1'], - }); -}); +router.get('/', getSubmissionsHandler); router.post('/:id', createSubmissionHandler); diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 0bf5c734..95c0f6fd 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -1,5 +1,5 @@ import { - getAnswerRepository, + getAnswerRepository, getAssignmentRepository, getClassRepository, getGroupRepository, getQuestionRepository @@ -13,8 +13,27 @@ import { LearningObjectIdentifier } from '../entities/content/learning-object-id import { mapToStudent } from '../interfaces/student.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; -import {AssignmentDTO} from "@dwengo-1/common/interfaces/assignment"; -import {mapToAssignment} from "../interfaces/assignment"; +import { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; +import { mapToAssignment } from "../interfaces/assignment"; + +export async function getQuestionsAboutLearningObjectInAssignment( + loId: LearningObjectIdentifier, + classId: string, + assignmentId: number, + full: boolean, + studentUsername?: string +): Promise { + const assignment = await getAssignmentRepository() + .findByClassIdAndAssignmentId(classId, assignmentId); + + const questions = await getQuestionRepository() + .findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, studentUsername); + + if (full) + return questions.map(q => mapToQuestionDTO(q)); + else + return questions.map(q => mapToQuestionDTOId(q)); +} export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { const questionRepository: QuestionRepository = getQuestionRepository(); diff --git a/backend/src/services/submissions.ts b/backend/src/services/submissions.ts index 1d8a7874..08a19481 100644 --- a/backend/src/services/submissions.ts +++ b/backend/src/services/submissions.ts @@ -1,4 +1,4 @@ -import { getSubmissionRepository } from '../data/repositories.js'; +import {getAssignmentRepository, getSubmissionRepository} from '../data/repositories.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js'; import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; @@ -55,3 +55,24 @@ export async function deleteSubmission( return submission; } + +/** + * Returns all the submissions made by on behalf of any group the given student is in. + */ +export async function getSubmissionsForLearningObjectAndAssignment( + learningObjectHruid: string, + language: Language, + version: number, + classId: string, + assignmentId: number, + studentUsername?: string +): Promise { + const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); + const assignment = await getAssignmentRepository() + .findByClassIdAndAssignmentId(classId, assignmentId); + + const submissions = await getSubmissionRepository() + .findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, studentUsername); + + return submissions.map(s => mapToSubmissionDTO(s)); +} From d21377cda43b5191aa85073de0fed68234f2c784 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 8 Apr 2025 00:59:36 +0200 Subject: [PATCH 09/71] feat(backend): Testen voor nieuwe functie in QuestionRepository toegevoegd. --- .../tests/data/questions/questions.test.ts | 60 ++++++++++++++++++- .../questions/questions.testdata.ts | 32 ++++++++-- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/backend/tests/data/questions/questions.test.ts b/backend/tests/data/questions/questions.test.ts index 567777f5..3ea71294 100644 --- a/backend/tests/data/questions/questions.test.ts +++ b/backend/tests/data/questions/questions.test.ts @@ -10,6 +10,9 @@ import { import { StudentRepository } from '../../../src/data/users/student-repository'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; import { Language } from '@dwengo-1/common/util/language'; +import {Question} from "../../../src/entities/questions/question.entity"; +import {Class} from "../../../src/entities/classes/class.entity"; +import {Assignment} from "../../../src/entities/assignments/assignment.entity"; describe('QuestionRepository', () => { let questionRepository: QuestionRepository; @@ -26,7 +29,7 @@ describe('QuestionRepository', () => { const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); expect(questions).toBeTruthy(); - expect(questions).toHaveLength(2); + expect(questions).toHaveLength(4); }); it('should create new question', async () => { @@ -48,6 +51,53 @@ describe('QuestionRepository', () => { expect(question).toHaveLength(1); }); + let clazz: Class | null; + let assignment: Assignment | null; + let loId: LearningObjectIdentifier; + it('should find all questions for a certain learning object and assignment', async () => { + clazz = await getClassRepository().findById('id01'); + assignment = await getAssignmentRepository().findByClassAndId(clazz!, 1); + loId = { + hruid: "id05", + language: Language.English, + version: 1 + }; + const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!); + sortQuestions(result); + + expect(result).toHaveLength(3); + + // question01: About learning object 'id05', in group #1 for Assignment #1 in class 'id01' + expect(result[0].learningObjectHruid).toEqual(loId.hruid); + expect(result[0].sequenceNumber).toEqual(1); + + // question02: About learning object 'id05', in group #1 for Assignment #1 in class 'id01' + expect(result[1].learningObjectHruid).toEqual(loId.hruid); + expect(result[1].sequenceNumber).toEqual(2); + + // question05: About learning object 'id05', in group #2 for Assignment #1 in class 'id01' + expect(result[2].learningObjectHruid).toEqual(loId.hruid); + expect(result[2].sequenceNumber).toEqual(3); + + // question06: About learning object 'id05', but for Assignment #2 in class 'id01' => not expected. + }); + + it("should find only the questions for a certain learning object and assignment asked by the user's group", async () => { + const result = + await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, "Tool"); + // (student Tool is in group #2) + + expect(result).toHaveLength(1); + + // question01 and question02 are in group #1 => not displayed. + + // question05: About learning object 'id05', in group #2 for Assignment #1 in class 'id01' + expect(result[0].learningObjectHruid).toEqual(loId.hruid); + expect(result[0].sequenceNumber).toEqual(3); + + // question06: About learning object 'id05', but for Assignment #2 in class 'id01' => not expected. + }); + it('should not find removed question', async () => { const id = new LearningObjectIdentifier('id04', Language.English, 1); await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(id, 1); @@ -57,3 +107,11 @@ describe('QuestionRepository', () => { expect(question).toHaveLength(0); }); }); + +function sortQuestions(questions: Question[]) { + questions.sort((a, b) => { + if (a.learningObjectHruid < b.learningObjectHruid) return -1 + else if (a.learningObjectHruid > b.learningObjectHruid) return 1 + else return a.sequenceNumber! - b.sequenceNumber! + }); +} diff --git a/backend/tests/test_assets/questions/questions.testdata.ts b/backend/tests/test_assets/questions/questions.testdata.ts index 0ccd498e..1e4a37ef 100644 --- a/backend/tests/test_assets/questions/questions.testdata.ts +++ b/backend/tests/test_assets/questions/questions.testdata.ts @@ -9,7 +9,7 @@ export function makeTestQuestions(em: EntityManager, students: Student[], groups learningObjectLanguage: Language.English, learningObjectVersion: 1, learningObjectHruid: 'id05', - inGroup: groups[0], + inGroup: groups[0], // Group #1 for Assignment #1 in class 'id01' sequenceNumber: 1, author: students[0], timestamp: new Date(), @@ -20,7 +20,7 @@ export function makeTestQuestions(em: EntityManager, students: Student[], groups learningObjectLanguage: Language.English, learningObjectVersion: 1, learningObjectHruid: 'id05', - inGroup: groups[0], + inGroup: groups[0], // Group #1 for Assignment #1 in class 'id01' sequenceNumber: 2, author: students[2], timestamp: new Date(), @@ -33,7 +33,7 @@ export function makeTestQuestions(em: EntityManager, students: Student[], groups learningObjectHruid: 'id04', sequenceNumber: 1, author: students[0], - inGroup: groups[0], + inGroup: groups[0], // Group #1 for Assignment #1 in class 'id01' timestamp: new Date(), content: 'question', }); @@ -44,10 +44,32 @@ export function makeTestQuestions(em: EntityManager, students: Student[], groups learningObjectHruid: 'id01', sequenceNumber: 1, author: students[1], - inGroup: groups[1], + inGroup: groups[1], // Group #2 for Assignment #1 in class 'id01' timestamp: new Date(), content: 'question', }); - return [question01, question02, question03, question04]; + const question05 = em.create(Question, { + learningObjectLanguage: Language.English, + learningObjectVersion: 1, + learningObjectHruid: 'id05', + sequenceNumber: 3, + author: students[1], + inGroup: groups[1], // Group #2 for Assignment #1 in class 'id01' + timestamp: new Date(), + content: 'question', + }); + + const question06 = em.create(Question, { + learningObjectLanguage: Language.English, + learningObjectVersion: 1, + learningObjectHruid: 'id05', + sequenceNumber: 4, + author: students[2], + inGroup: groups[3], // Group #4 for Assignment #2 in class 'id02' + timestamp: new Date(), + content: 'question', + }); + + return [question01, question02, question03, question04, question05, question06]; } From 3c3a1d89c6c37dcdc78b09620dcc27e842aa6985 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 8 Apr 2025 01:05:27 +0200 Subject: [PATCH 10/71] style(backend): Lint --- backend/src/controllers/submissions.ts | 4 ++-- .../data/assignments/submission-repository.ts | 2 +- .../src/data/questions/question-repository.ts | 2 +- backend/src/interfaces/group.ts | 2 +- backend/src/services/questions.ts | 5 ++--- .../data/assignments/submissions.test.ts | 14 ++++++------- .../tests/data/questions/questions.test.ts | 20 +++++++++---------- .../assignments/submission.testdata.ts | 8 ++++---- 8 files changed, 28 insertions(+), 29 deletions(-) diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index 3a5aa391..ac6c3bb9 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -30,8 +30,8 @@ export async function getSubmissionsHandler( res: Response ): Promise { const loHruid = req.params.hruid; - const lang = languageMap[req.query.language as string] || Language.Dutch; - const version = (req.query.version || 1) as number; + const lang = languageMap[req.query.language] || Language.Dutch; + const version = (req.query.version || 1); const submissions = await getSubmissionsForLearningObjectAndAssignment( loHruid, lang, version, req.query.classId, req.query.assignmentId diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index 00cdac14..89832899 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -60,7 +60,7 @@ export class SubmissionRepository extends DwengoEntityRepository { assignment: Assignment, forStudentUsername?: string ): Promise { - let onBehalfOf = forStudentUsername ? { + const onBehalfOf = forStudentUsername ? { assignment, members: { $some: { diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index f36b1074..714c3818 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -75,7 +75,7 @@ export class QuestionRepository extends DwengoEntityRepository { assignment: Assignment, forStudentUsername?: string ): Promise { - let inGroup = forStudentUsername ? { + const inGroup = forStudentUsername ? { assignment, members: { $some: { diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts index da3ad845..d21762db 100644 --- a/backend/src/interfaces/group.ts +++ b/backend/src/interfaces/group.ts @@ -12,7 +12,7 @@ export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { return getGroupRepository().create({ groupNumber: groupDto.groupNumber, - assignment: mapToAssignment(assignmentDto, clazz!), + assignment: mapToAssignment(assignmentDto, clazz), members: groupDto.members.map(studentDto => mapToStudent(studentDto as StudentDTO)) }); } diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 95c0f6fd..5f5c22b7 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -30,9 +30,8 @@ export async function getQuestionsAboutLearningObjectInAssignment( .findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, studentUsername); if (full) - return questions.map(q => mapToQuestionDTO(q)); - else - return questions.map(q => mapToQuestionDTOId(q)); + {return questions.map(q => mapToQuestionDTO(q));} + return questions.map(q => mapToQuestionDTOId(q)); } export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { diff --git a/backend/tests/data/assignments/submissions.test.ts b/backend/tests/data/assignments/submissions.test.ts index a723e86f..2aa43f4c 100644 --- a/backend/tests/data/assignments/submissions.test.ts +++ b/backend/tests/data/assignments/submissions.test.ts @@ -97,15 +97,15 @@ describe('SubmissionRepository', () => { expect(result).toHaveLength(3); - // submission3 should be found (for learning object 'id02' by group #1 for Assignment #1 in class 'id01') + // Submission3 should be found (for learning object 'id02' by group #1 for Assignment #1 in class 'id01') expect(result[0].learningObjectHruid).toBe(loId.hruid); expect(result[0].submissionNumber).toBe(1); - // submission4 should be found (for learning object 'id02' by group #1 for Assignment #1 in class 'id01') + // Submission4 should be found (for learning object 'id02' by group #1 for Assignment #1 in class 'id01') expect(result[1].learningObjectHruid).toBe(loId.hruid); expect(result[1].submissionNumber).toBe(2); - // submission8 should be found (for learning object 'id02' by group #2 for Assignment #1 in class 'id01') + // Submission8 should be found (for learning object 'id02' by group #2 for Assignment #1 in class 'id01') expect(result[2].learningObjectHruid).toBe(loId.hruid); expect(result[2].submissionNumber).toBe(3); }); @@ -117,12 +117,12 @@ describe('SubmissionRepository', () => { expect(result).toHaveLength(1); - // submission8 should be found (for learning object 'id02' by group #2 for Assignment #1 in class 'id01') + // Submission8 should be found (for learning object 'id02' by group #2 for Assignment #1 in class 'id01') expect(result[0].learningObjectHruid).toBe(loId.hruid); expect(result[0].submissionNumber).toBe(3); // The other submissions found in the previous test case should not be found anymore as they were made on - // behalf of group #1 which Tool is no member of. + // Behalf of group #1 which Tool is no member of. }); it('should not find a deleted submission', async () => { @@ -137,8 +137,8 @@ describe('SubmissionRepository', () => { function sortSubmissions(submissions: Submission[]) { submissions.sort((a, b) => { - if (a.learningObjectHruid < b.learningObjectHruid) return -1; - if (a.learningObjectHruid > b.learningObjectHruid) return 1; + if (a.learningObjectHruid < b.learningObjectHruid) {return -1;} + if (a.learningObjectHruid > b.learningObjectHruid) {return 1;} return a.submissionNumber! - b.submissionNumber!; }); } diff --git a/backend/tests/data/questions/questions.test.ts b/backend/tests/data/questions/questions.test.ts index 3ea71294..34a3852c 100644 --- a/backend/tests/data/questions/questions.test.ts +++ b/backend/tests/data/questions/questions.test.ts @@ -67,19 +67,19 @@ describe('QuestionRepository', () => { expect(result).toHaveLength(3); - // question01: About learning object 'id05', in group #1 for Assignment #1 in class 'id01' + // Question01: About learning object 'id05', in group #1 for Assignment #1 in class 'id01' expect(result[0].learningObjectHruid).toEqual(loId.hruid); expect(result[0].sequenceNumber).toEqual(1); - // question02: About learning object 'id05', in group #1 for Assignment #1 in class 'id01' + // Question02: About learning object 'id05', in group #1 for Assignment #1 in class 'id01' expect(result[1].learningObjectHruid).toEqual(loId.hruid); expect(result[1].sequenceNumber).toEqual(2); - // question05: About learning object 'id05', in group #2 for Assignment #1 in class 'id01' + // Question05: About learning object 'id05', in group #2 for Assignment #1 in class 'id01' expect(result[2].learningObjectHruid).toEqual(loId.hruid); expect(result[2].sequenceNumber).toEqual(3); - // question06: About learning object 'id05', but for Assignment #2 in class 'id01' => not expected. + // Question06: About learning object 'id05', but for Assignment #2 in class 'id01' => not expected. }); it("should find only the questions for a certain learning object and assignment asked by the user's group", async () => { @@ -89,13 +89,13 @@ describe('QuestionRepository', () => { expect(result).toHaveLength(1); - // question01 and question02 are in group #1 => not displayed. + // Question01 and question02 are in group #1 => not displayed. - // question05: About learning object 'id05', in group #2 for Assignment #1 in class 'id01' + // Question05: About learning object 'id05', in group #2 for Assignment #1 in class 'id01' expect(result[0].learningObjectHruid).toEqual(loId.hruid); expect(result[0].sequenceNumber).toEqual(3); - // question06: About learning object 'id05', but for Assignment #2 in class 'id01' => not expected. + // Question06: About learning object 'id05', but for Assignment #2 in class 'id01' => not expected. }); it('should not find removed question', async () => { @@ -110,8 +110,8 @@ describe('QuestionRepository', () => { function sortQuestions(questions: Question[]) { questions.sort((a, b) => { - if (a.learningObjectHruid < b.learningObjectHruid) return -1 - else if (a.learningObjectHruid > b.learningObjectHruid) return 1 - else return a.sequenceNumber! - b.sequenceNumber! + if (a.learningObjectHruid < b.learningObjectHruid) {return -1} + else if (a.learningObjectHruid > b.learningObjectHruid) {return 1} + return a.sequenceNumber! - b.sequenceNumber! }); } diff --git a/backend/tests/test_assets/assignments/submission.testdata.ts b/backend/tests/test_assets/assignments/submission.testdata.ts index 812d1289..81db2229 100644 --- a/backend/tests/test_assets/assignments/submission.testdata.ts +++ b/backend/tests/test_assets/assignments/submission.testdata.ts @@ -12,7 +12,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 1, submitter: students[0], submissionTime: new Date(2025, 2, 20), - onBehalfOf: groups[0], // group #1 for Assignment #1 in class 'id01' + onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01' content: 'sub1', }); @@ -23,7 +23,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 2, submitter: students[0], submissionTime: new Date(2025, 2, 25), - onBehalfOf: groups[0], // group #1 for Assignment #1 in class 'id01' + onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01' content: '', }); @@ -34,7 +34,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 1, submitter: students[0], submissionTime: new Date(2025, 2, 20), - onBehalfOf: groups[0], // group #1 for Assignment #1 in class 'id01' + onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01' content: '', }); @@ -45,7 +45,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou submissionNumber: 2, submitter: students[0], submissionTime: new Date(2025, 2, 25), - onBehalfOf: groups[0], // group #1 for Assignment #1 in class 'id01' + onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01' content: '', }); From fc675710b4171bf15c2065616e4d02a396bd0c5a Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 8 Apr 2025 10:25:30 +0200 Subject: [PATCH 11/71] fix(backend): Falende testen gerepareerd. --- .../data/assignments/submission-repository.ts | 11 +++++++++-- backend/src/interfaces/group.ts | 16 +++++++++++++--- backend/src/interfaces/question.ts | 4 ++-- backend/src/services/groups.ts | 6 +++--- backend/src/services/students.ts | 8 +++++--- backend/tests/controllers/students.test.ts | 1 + common/src/interfaces/group.ts | 2 +- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index 89832899..a79dc417 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -82,12 +82,19 @@ export class SubmissionRepository extends DwengoEntityRepository { } public async findAllSubmissionsForStudent(student: Student): Promise { - return this.find( + const result = await this.find( { submitter: student }, { - populate: ["onBehalfOf.members"] + populate: [ + "onBehalfOf.members" + ] } ); + + // Workaround: For some reason, without this MikroORM generates an UPDATE query with a syntax error in some tests + this.em.clear(); + + return result; } public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts index d21762db..d3785137 100644 --- a/backend/src/interfaces/group.ts +++ b/backend/src/interfaces/group.ts @@ -1,7 +1,7 @@ import { Group } from '../entities/assignments/group.entity.js'; -import {mapToAssignment, mapToAssignmentDTO} from './assignment.js'; +import {mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId} from './assignment.js'; import {mapToStudent, mapToStudentDTO} from './student.js'; -import { GroupDTO } from '@dwengo-1/common/interfaces/group'; +import {GroupDTO} from '@dwengo-1/common/interfaces/group'; import {getGroupRepository} from "../data/repositories"; import {AssignmentDTO} from "@dwengo-1/common/interfaces/assignment"; import {Class} from "../entities/classes/class.entity"; @@ -13,7 +13,7 @@ export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { return getGroupRepository().create({ groupNumber: groupDto.groupNumber, assignment: mapToAssignment(assignmentDto, clazz), - members: groupDto.members.map(studentDto => mapToStudent(studentDto as StudentDTO)) + members: groupDto.members!.map(studentDto => mapToStudent(studentDto as StudentDTO)) }); } @@ -26,6 +26,16 @@ export function mapToGroupDTO(group: Group): GroupDTO { } export function mapToGroupDTOId(group: Group): GroupDTO { + return { + assignment: mapToAssignmentDTOId(group.assignment), + groupNumber: group.groupNumber!, + }; +} + +/** + * Map to group DTO where other objects are only referenced by their id. + */ +export function mapToShallowGroupDTO(group: Group): GroupDTO { return { assignment: group.assignment.id!, groupNumber: group.groupNumber!, diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 91b90796..5419618a 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -2,7 +2,7 @@ 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'; -import {mapToGroupDTO} from "./group"; +import { mapToGroupDTOId } from "./group"; function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { return { @@ -22,7 +22,7 @@ export function mapToQuestionDTO(question: Question): QuestionDTO { learningObjectIdentifier, sequenceNumber: question.sequenceNumber!, author: mapToStudentDTO(question.author), - inGroup: mapToGroupDTO(question.inGroup), + inGroup: mapToGroupDTOId(question.inGroup), timestamp: question.timestamp.toISOString(), content: question.content, }; diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 346c1ee1..b009e772 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -6,7 +6,7 @@ import { getSubmissionRepository, } from '../data/repositories.js'; import { Group } from '../entities/assignments/group.entity.js'; -import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; +import { mapToGroupDTO, mapToShallowGroupDTO } from '../interfaces/group.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; import { GroupDTO } from '@dwengo-1/common/interfaces/group'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; @@ -38,7 +38,7 @@ export async function getGroup(classId: string, assignmentNumber: number, groupN return mapToGroupDTO(group); } - return mapToGroupDTOId(group); + return mapToShallowGroupDTO(group); } export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise { @@ -103,7 +103,7 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu return groups.map(mapToGroupDTO); } - return groups.map(mapToGroupDTOId); + return groups.map(mapToShallowGroupDTO); } export async function getGroupSubmissions( diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index dc40e468..be2d270b 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -7,7 +7,7 @@ import { getSubmissionRepository, } from '../data/repositories.js'; import { mapToClassDTO } from '../interfaces/class.js'; -import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; +import { mapToGroupDTO, mapToShallowGroupDTO } from '../interfaces/group.js'; import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; import { getAllAssignments } from './assignments.js'; @@ -23,6 +23,7 @@ import { GroupDTO } from '@dwengo-1/common/interfaces/group'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; +import {Submission} from "../entities/assignments/submission.entity"; export async function getAllStudents(full: boolean): Promise { const studentRepository = getStudentRepository(); @@ -100,14 +101,15 @@ export async function getStudentGroups(username: string, full: boolean): Promise return groups.map(mapToGroupDTO); } - return groups.map(mapToGroupDTOId); + return groups.map(mapToShallowGroupDTO); } export async function getStudentSubmissions(username: string, full: boolean): Promise { const student = await fetchStudent(username); const submissionRepository = getSubmissionRepository(); - const submissions = await submissionRepository.findAllSubmissionsForStudent(student); + + const submissions: Submission[] = await submissionRepository.findAllSubmissionsForStudent(student); if (full) { return submissions.map(mapToSubmissionDTO); diff --git a/backend/tests/controllers/students.test.ts b/backend/tests/controllers/students.test.ts index 93f35c48..35f9a9cf 100644 --- a/backend/tests/controllers/students.test.ts +++ b/backend/tests/controllers/students.test.ts @@ -147,6 +147,7 @@ describe('Student controllers', () => { const result = jsonMock.mock.lastCall?.[0]; expect(result.submissions).to.have.length.greaterThan(0); + }); it('Student questions', async () => { diff --git a/common/src/interfaces/group.ts b/common/src/interfaces/group.ts index ca95770a..16e22780 100644 --- a/common/src/interfaces/group.ts +++ b/common/src/interfaces/group.ts @@ -4,5 +4,5 @@ import { StudentDTO } from './student'; export interface GroupDTO { assignment: number | AssignmentDTO; groupNumber: number; - members: string[] | StudentDTO[]; + members?: string[] | StudentDTO[]; } From ba725f67b25fb64c0d5596c9c0f9c745c5f8b40e Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 8 Apr 2025 10:46:26 +0200 Subject: [PATCH 12/71] fix(backend): Falende testen gerepareerd. --- backend/src/controllers/questions.ts | 10 +++++++-- .../data/assignments/assignments.test.ts | 4 +++- .../data/assignments/submissions.test.ts | 21 +------------------ .../tests/data/questions/questions.test.ts | 2 +- 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index da1d020e..f1426b38 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -22,7 +22,10 @@ interface QuestionQueryParams { lang: string; } -function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { +function getObjectId( + req: Request, + res: Response +): LearningObjectIdentifier | null { const { hruid, version } = req.params; const lang = req.query.lang; @@ -41,7 +44,10 @@ function getObjectId(req: Request, res: Response): QuestionId | null { +function getQuestionId( + req: Request, + res: Response +): QuestionId | null { const seq = req.params.seq; const learningObjectIdentifier = getObjectId(req, res); diff --git a/backend/tests/data/assignments/assignments.test.ts b/backend/tests/data/assignments/assignments.test.ts index f587e979..aad084e3 100644 --- a/backend/tests/data/assignments/assignments.test.ts +++ b/backend/tests/data/assignments/assignments.test.ts @@ -33,7 +33,9 @@ describe('AssignmentRepository', () => { it('should find all by username of the responsible teacher', async () => { const result = await assignmentRepository.findAllByResponsibleTeacher("FooFighters") - const resultIds = result.map(it => it.id).sort(); + const resultIds = result + .map(it => it.id) + .sort((a, b) => (a ?? 0) - (b ?? 0)); expect(resultIds).toEqual([1, 3, 4]); }); diff --git a/backend/tests/data/assignments/submissions.test.ts b/backend/tests/data/assignments/submissions.test.ts index 2aa43f4c..c7920734 100644 --- a/backend/tests/data/assignments/submissions.test.ts +++ b/backend/tests/data/assignments/submissions.test.ts @@ -18,25 +18,6 @@ import {Submission} from "../../../src/entities/assignments/submission.entity"; import {Class} from "../../../src/entities/classes/class.entity"; import {Assignment} from "../../../src/entities/assignments/assignment.entity"; -export function checkSubmissionsForStudentNoordkaap(result: Submission[]) { - sortSubmissions(result); - - expect(result[0].learningObjectHruid).toBe("id01"); - expect(result[0].submissionNumber).toBe(2); - - expect(result[1].learningObjectHruid).toBe("id02"); - expect(result[1].submissionNumber).toBe(1); - - expect(result[2].learningObjectHruid).toBe("id02"); - expect(result[2].submissionNumber).toBe(2); - - expect(result[3].learningObjectHruid).toBe("id03"); - expect(result[3].submissionNumber).toBe(1); - - expect(result[4].learningObjectHruid).toBe("id03"); - expect(result[4].submissionNumber).toBe(2); -} - describe('SubmissionRepository', () => { let submissionRepository: SubmissionRepository; let studentRepository: StudentRepository; @@ -135,7 +116,7 @@ describe('SubmissionRepository', () => { }); }); -function sortSubmissions(submissions: Submission[]) { +function sortSubmissions(submissions: Submission[]): void { submissions.sort((a, b) => { if (a.learningObjectHruid < b.learningObjectHruid) {return -1;} if (a.learningObjectHruid > b.learningObjectHruid) {return 1;} diff --git a/backend/tests/data/questions/questions.test.ts b/backend/tests/data/questions/questions.test.ts index 34a3852c..e8069070 100644 --- a/backend/tests/data/questions/questions.test.ts +++ b/backend/tests/data/questions/questions.test.ts @@ -108,7 +108,7 @@ describe('QuestionRepository', () => { }); }); -function sortQuestions(questions: Question[]) { +function sortQuestions(questions: Question[]): void { questions.sort((a, b) => { if (a.learningObjectHruid < b.learningObjectHruid) {return -1} else if (a.learningObjectHruid > b.learningObjectHruid) {return 1} From 8a55c0f0038c8f424347471acc9a3c47335b2592 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Tue, 8 Apr 2025 08:56:50 +0000 Subject: [PATCH 13/71] style: fix linting issues met Prettier --- backend/src/controllers/questions.ts | 16 ++++---- backend/src/controllers/submissions.ts | 28 +++++-------- .../data/assignments/assignment-repository.ts | 10 ++--- .../data/assignments/submission-repository.ts | 34 ++++++++-------- .../src/data/questions/question-repository.ts | 32 ++++++++------- .../entities/assignments/assignment.entity.ts | 2 +- .../src/entities/assignments/group.entity.ts | 2 +- .../entities/assignments/submission.entity.ts | 4 +- .../src/entities/questions/question.entity.ts | 2 +- backend/src/interfaces/group.ts | 16 ++++---- backend/src/interfaces/question.ts | 2 +- backend/src/routes/submissions.ts | 7 +--- backend/src/services/questions.ts | 26 +++++-------- backend/src/services/students.ts | 2 +- backend/src/services/submissions.ts | 10 ++--- backend/tests/controllers/students.test.ts | 1 - .../data/assignments/assignments.test.ts | 6 +-- .../data/assignments/submissions.test.ts | 39 ++++++++++--------- .../tests/data/questions/questions.test.ts | 29 +++++++------- .../database-learning-path-provider.test.ts | 22 +++++------ .../questions/questions.testdata.ts | 2 +- common/src/interfaces/question.ts | 2 +- 22 files changed, 137 insertions(+), 157 deletions(-) diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index f1426b38..26efb4f4 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -5,13 +5,13 @@ import { getAllQuestions, getAnswersByQuestion, getQuestion, - getQuestionsAboutLearningObjectInAssignment + getQuestionsAboutLearningObjectInAssignment, } from '../services/questions.js'; import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { Language } from '@dwengo-1/common/util/language'; -import {AnswerDTO, AnswerId} from "@dwengo-1/common/interfaces/answer"; +import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; interface QuestionPathParams { hruid: string; @@ -62,10 +62,10 @@ function getQuestionId( } interface GetAllQuestionsQueryParams extends QuestionQueryParams { - classId?: string, - assignmentId?: number, - forStudent?: string, - full?: boolean + classId?: string; + assignmentId?: number; + forStudent?: string; + full?: boolean; } export async function getAllQuestionsHandler( @@ -118,10 +118,10 @@ export async function getQuestionHandler( } interface GetQuestionAnswersQueryParams extends QuestionQueryParams { - full: boolean + full: boolean; } export async function getQuestionAnswersHandler( - req: Request, + req: Request, res: Response ): Promise { const questionId = getQuestionId(req, res); diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index ac6c3bb9..73f1317f 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -1,13 +1,8 @@ import { Request, Response } from 'express'; -import { - createSubmission, - deleteSubmission, - getSubmission, - getSubmissionsForLearningObjectAndAssignment -} from '../services/submissions.js'; +import { createSubmission, deleteSubmission, getSubmission, getSubmissionsForLearningObjectAndAssignment } from '../services/submissions.js'; import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; import { Language, languageMap } from '@dwengo-1/common/util/language'; -import {Submission} from "../entities/assignments/submission.entity"; +import { Submission } from '../entities/assignments/submission.entity'; interface SubmissionParams { hruid: string; @@ -15,27 +10,22 @@ interface SubmissionParams { } interface SubmissionQuery { - language: string, + language: string; version: number; } interface SubmissionsQuery extends SubmissionQuery { - classId: string, - assignmentId: number, - studentUsername?: string + classId: string; + assignmentId: number; + studentUsername?: string; } -export async function getSubmissionsHandler( - req: Request, - res: Response -): Promise { +export async function getSubmissionsHandler(req: Request, res: Response): Promise { const loHruid = req.params.hruid; const lang = languageMap[req.query.language] || Language.Dutch; - const version = (req.query.version || 1); + const version = req.query.version || 1; - const submissions = await getSubmissionsForLearningObjectAndAssignment( - loHruid, lang, version, req.query.classId, req.query.assignmentId - ); + const submissions = await getSubmissionsForLearningObjectAndAssignment(loHruid, lang, version, req.query.classId, req.query.assignmentId); res.json(submissions); } diff --git a/backend/src/data/assignments/assignment-repository.ts b/backend/src/data/assignments/assignment-repository.ts index c6766af9..db12a74f 100644 --- a/backend/src/data/assignments/assignment-repository.ts +++ b/backend/src/data/assignments/assignment-repository.ts @@ -15,11 +15,11 @@ export class AssignmentRepository extends DwengoEntityRepository { within: { teachers: { $some: { - username: teacherUsername - } - } - } - } + username: teacherUsername, + }, + }, + }, + }, }); } public async findAllAssignmentsInClass(within: Class): Promise { diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index a79dc417..c82ed9c3 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -3,7 +3,7 @@ import { Group } from '../../entities/assignments/group.entity.js'; import { Submission } from '../../entities/assignments/submission.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { Student } from '../../entities/users/student.entity.js'; -import {Assignment} from "../../entities/assignments/assignment.entity"; +import { Assignment } from '../../entities/assignments/assignment.entity'; export class SubmissionRepository extends DwengoEntityRepository { public async findSubmissionByLearningObjectAndSubmissionNumber( @@ -46,7 +46,7 @@ export class SubmissionRepository extends DwengoEntityRepository { return this.find( { onBehalfOf: group }, { - populate: ["onBehalfOf.members"] + populate: ['onBehalfOf.members'], } ); } @@ -60,24 +60,26 @@ export class SubmissionRepository extends DwengoEntityRepository { assignment: Assignment, forStudentUsername?: string ): Promise { - const onBehalfOf = forStudentUsername ? { - assignment, - members: { - $some: { - username: forStudentUsername - } - } - } : { - assignment - }; + const onBehalfOf = forStudentUsername + ? { + assignment, + members: { + $some: { + username: forStudentUsername, + }, + }, + } + : { + assignment, + }; return this.findAll({ where: { learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, learningObjectVersion: loId.version, - onBehalfOf - } + onBehalfOf, + }, }); } @@ -85,9 +87,7 @@ export class SubmissionRepository extends DwengoEntityRepository { const result = await this.find( { submitter: student }, { - populate: [ - "onBehalfOf.members" - ] + populate: ['onBehalfOf.members'], } ); diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 714c3818..6b961e07 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -3,11 +3,11 @@ import { Question } from '../../entities/questions/question.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { Student } from '../../entities/users/student.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; -import {Group} from "../../entities/assignments/group.entity"; -import {Assignment} from "../../entities/assignments/assignment.entity"; +import { Group } from '../../entities/assignments/group.entity'; +import { Assignment } from '../../entities/assignments/assignment.entity'; export class QuestionRepository extends DwengoEntityRepository { - public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group, content: string }): Promise { + public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise { const questionEntity = this.create({ learningObjectHruid: question.loId.hruid, learningObjectLanguage: question.loId.language, @@ -75,24 +75,26 @@ export class QuestionRepository extends DwengoEntityRepository { assignment: Assignment, forStudentUsername?: string ): Promise { - const inGroup = forStudentUsername ? { - assignment, - members: { - $some: { - username: forStudentUsername - } - } - } : { - assignment - }; + const inGroup = forStudentUsername + ? { + assignment, + members: { + $some: { + username: forStudentUsername, + }, + }, + } + : { + assignment, + }; return this.findAll({ where: { learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, learningObjectVersion: loId.version, - inGroup - } + inGroup, + }, }); } } diff --git a/backend/src/entities/assignments/assignment.entity.ts b/backend/src/entities/assignments/assignment.entity.ts index 52773909..e3f75489 100644 --- a/backend/src/entities/assignments/assignment.entity.ts +++ b/backend/src/entities/assignments/assignment.entity.ts @@ -1,4 +1,4 @@ -import {Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property} from '@mikro-orm/core'; +import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Class } from '../classes/class.entity.js'; import { Group } from './group.entity.js'; import { Language } from '@dwengo-1/common/util/language'; diff --git a/backend/src/entities/assignments/group.entity.ts b/backend/src/entities/assignments/group.entity.ts index 1a69ed4b..55770b7f 100644 --- a/backend/src/entities/assignments/group.entity.ts +++ b/backend/src/entities/assignments/group.entity.ts @@ -1,4 +1,4 @@ -import {Collection, Entity, ManyToMany, ManyToOne, PrimaryKey} from '@mikro-orm/core'; +import { Collection, Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; import { Assignment } from './assignment.entity.js'; import { Student } from '../users/student.entity.js'; import { GroupRepository } from '../../data/assignments/group-repository.js'; diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index 728dd436..82d49a40 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -22,7 +22,7 @@ export class Submission { submissionNumber?: number; @ManyToOne({ - entity: () => Group + entity: () => Group, }) onBehalfOf!: Group; @@ -34,8 +34,6 @@ export class Submission { @Property({ type: 'datetime' }) submissionTime!: Date; - - @Property({ type: 'json' }) content!: string; } diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index c6df4e6a..44ff3e32 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -2,7 +2,7 @@ import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; import { Student } from '../users/student.entity.js'; import { QuestionRepository } from '../../data/questions/question-repository.js'; import { Language } from '@dwengo-1/common/util/language'; -import {Group} from "../assignments/group.entity"; +import { Group } from '../assignments/group.entity'; @Entity({ repository: () => QuestionRepository }) export class Question { diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts index d3785137..295c7e0f 100644 --- a/backend/src/interfaces/group.ts +++ b/backend/src/interfaces/group.ts @@ -1,11 +1,11 @@ import { Group } from '../entities/assignments/group.entity.js'; -import {mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId} from './assignment.js'; -import {mapToStudent, mapToStudentDTO} from './student.js'; -import {GroupDTO} from '@dwengo-1/common/interfaces/group'; -import {getGroupRepository} from "../data/repositories"; -import {AssignmentDTO} from "@dwengo-1/common/interfaces/assignment"; -import {Class} from "../entities/classes/class.entity"; -import {StudentDTO} from "@dwengo-1/common/interfaces/student"; +import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from './assignment.js'; +import { mapToStudent, mapToStudentDTO } from './student.js'; +import { GroupDTO } from '@dwengo-1/common/interfaces/group'; +import { getGroupRepository } from '../data/repositories'; +import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; +import { Class } from '../entities/classes/class.entity'; +import { StudentDTO } from '@dwengo-1/common/interfaces/student'; export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { const assignmentDto = groupDto.assignment as AssignmentDTO; @@ -13,7 +13,7 @@ export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { return getGroupRepository().create({ groupNumber: groupDto.groupNumber, assignment: mapToAssignment(assignmentDto, clazz), - members: groupDto.members!.map(studentDto => mapToStudent(studentDto as StudentDTO)) + members: groupDto.members!.map((studentDto) => mapToStudent(studentDto as StudentDTO)), }); } diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 5419618a..a9347047 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -2,7 +2,7 @@ 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'; -import { mapToGroupDTOId } from "./group"; +import { mapToGroupDTOId } from './group'; function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { return { diff --git a/backend/src/routes/submissions.ts b/backend/src/routes/submissions.ts index 930e1a59..492b6439 100644 --- a/backend/src/routes/submissions.ts +++ b/backend/src/routes/submissions.ts @@ -1,10 +1,5 @@ import express from 'express'; -import { - createSubmissionHandler, - deleteSubmissionHandler, - getSubmissionHandler, - getSubmissionsHandler -} from '../controllers/submissions.js'; +import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.js'; const router = express.Router({ mergeParams: true }); // Root endpoint used to search objects diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 5f5c22b7..aa3e1298 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -1,9 +1,4 @@ -import { - getAnswerRepository, getAssignmentRepository, - getClassRepository, - getGroupRepository, - getQuestionRepository -} from '../data/repositories.js'; +import { getAnswerRepository, getAssignmentRepository, getClassRepository, getGroupRepository, getQuestionRepository } from '../data/repositories.js'; import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; import { Question } from '../entities/questions/question.entity.js'; import { Answer } from '../entities/questions/answer.entity.js'; @@ -13,8 +8,8 @@ import { LearningObjectIdentifier } from '../entities/content/learning-object-id import { mapToStudent } from '../interfaces/student.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; -import { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; -import { mapToAssignment } from "../interfaces/assignment"; +import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; +import { mapToAssignment } from '../interfaces/assignment'; export async function getQuestionsAboutLearningObjectInAssignment( loId: LearningObjectIdentifier, @@ -23,15 +18,14 @@ export async function getQuestionsAboutLearningObjectInAssignment( full: boolean, studentUsername?: string ): Promise { - const assignment = await getAssignmentRepository() - .findByClassIdAndAssignmentId(classId, assignmentId); + const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, assignmentId); - const questions = await getQuestionRepository() - .findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, studentUsername); + const questions = await getQuestionRepository().findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, studentUsername); - if (full) - {return questions.map(q => mapToQuestionDTO(q));} - return questions.map(q => mapToQuestionDTOId(q)); + if (full) { + return questions.map((q) => mapToQuestionDTO(q)); + } + return questions.map((q) => mapToQuestionDTOId(q)); } export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { @@ -101,7 +95,7 @@ export async function createQuestion(questionDTO: QuestionDTO): Promise { const studentRepository = getStudentRepository(); diff --git a/backend/src/services/submissions.ts b/backend/src/services/submissions.ts index 08a19481..23659d63 100644 --- a/backend/src/services/submissions.ts +++ b/backend/src/services/submissions.ts @@ -1,4 +1,4 @@ -import {getAssignmentRepository, getSubmissionRepository} from '../data/repositories.js'; +import { getAssignmentRepository, getSubmissionRepository } from '../data/repositories.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js'; import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; @@ -68,11 +68,9 @@ export async function getSubmissionsForLearningObjectAndAssignment( studentUsername?: string ): Promise { const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); - const assignment = await getAssignmentRepository() - .findByClassIdAndAssignmentId(classId, assignmentId); + const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, assignmentId); - const submissions = await getSubmissionRepository() - .findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, studentUsername); + const submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, studentUsername); - return submissions.map(s => mapToSubmissionDTO(s)); + return submissions.map((s) => mapToSubmissionDTO(s)); } diff --git a/backend/tests/controllers/students.test.ts b/backend/tests/controllers/students.test.ts index 35f9a9cf..93f35c48 100644 --- a/backend/tests/controllers/students.test.ts +++ b/backend/tests/controllers/students.test.ts @@ -147,7 +147,6 @@ describe('Student controllers', () => { const result = jsonMock.mock.lastCall?.[0]; expect(result.submissions).to.have.length.greaterThan(0); - }); it('Student questions', async () => { diff --git a/backend/tests/data/assignments/assignments.test.ts b/backend/tests/data/assignments/assignments.test.ts index aad084e3..2bad08f2 100644 --- a/backend/tests/data/assignments/assignments.test.ts +++ b/backend/tests/data/assignments/assignments.test.ts @@ -32,10 +32,8 @@ describe('AssignmentRepository', () => { }); it('should find all by username of the responsible teacher', async () => { - const result = await assignmentRepository.findAllByResponsibleTeacher("FooFighters") - const resultIds = result - .map(it => it.id) - .sort((a, b) => (a ?? 0) - (b ?? 0)); + const result = await assignmentRepository.findAllByResponsibleTeacher('FooFighters'); + const resultIds = result.map((it) => it.id).sort((a, b) => (a ?? 0) - (b ?? 0)); expect(resultIds).toEqual([1, 3, 4]); }); diff --git a/backend/tests/data/assignments/submissions.test.ts b/backend/tests/data/assignments/submissions.test.ts index c7920734..acc82384 100644 --- a/backend/tests/data/assignments/submissions.test.ts +++ b/backend/tests/data/assignments/submissions.test.ts @@ -1,6 +1,6 @@ -import {beforeAll, describe, expect, it} from 'vitest'; -import {setupTestApp} from '../../setup-tests'; -import {SubmissionRepository} from '../../../src/data/assignments/submission-repository'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { setupTestApp } from '../../setup-tests'; +import { SubmissionRepository } from '../../../src/data/assignments/submission-repository'; import { getAssignmentRepository, getClassRepository, @@ -8,15 +8,15 @@ import { getStudentRepository, getSubmissionRepository, } from '../../../src/data/repositories'; -import {LearningObjectIdentifier} from '../../../src/entities/content/learning-object-identifier'; -import {Language} from '@dwengo-1/common/util/language'; -import {StudentRepository} from '../../../src/data/users/student-repository'; -import {GroupRepository} from '../../../src/data/assignments/group-repository'; -import {AssignmentRepository} from '../../../src/data/assignments/assignment-repository'; -import {ClassRepository} from '../../../src/data/classes/class-repository'; -import {Submission} from "../../../src/entities/assignments/submission.entity"; -import {Class} from "../../../src/entities/classes/class.entity"; -import {Assignment} from "../../../src/entities/assignments/assignment.entity"; +import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; +import { Language } from '@dwengo-1/common/util/language'; +import { StudentRepository } from '../../../src/data/users/student-repository'; +import { GroupRepository } from '../../../src/data/assignments/group-repository'; +import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository'; +import { ClassRepository } from '../../../src/data/classes/class-repository'; +import { Submission } from '../../../src/entities/assignments/submission.entity'; +import { Class } from '../../../src/entities/classes/class.entity'; +import { Assignment } from '../../../src/entities/assignments/assignment.entity'; describe('SubmissionRepository', () => { let submissionRepository: SubmissionRepository; @@ -69,9 +69,9 @@ describe('SubmissionRepository', () => { clazz = await classRepository.findById('id01'); assignment = await assignmentRepository.findByClassAndId(clazz!, 1); loId = { - hruid: "id02", + hruid: 'id02', language: Language.English, - version: 1 + version: 1, }; const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!); sortSubmissions(result); @@ -92,8 +92,7 @@ describe('SubmissionRepository', () => { }); it("should find only the submissions for a certain learning object and assignment made for the user's group", async () => { - const result = - await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, "Tool"); + const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, 'Tool'); // (student Tool is in group #2) expect(result).toHaveLength(1); @@ -118,8 +117,12 @@ describe('SubmissionRepository', () => { function sortSubmissions(submissions: Submission[]): void { submissions.sort((a, b) => { - if (a.learningObjectHruid < b.learningObjectHruid) {return -1;} - if (a.learningObjectHruid > b.learningObjectHruid) {return 1;} + if (a.learningObjectHruid < b.learningObjectHruid) { + return -1; + } + if (a.learningObjectHruid > b.learningObjectHruid) { + return 1; + } return a.submissionNumber! - b.submissionNumber!; }); } diff --git a/backend/tests/data/questions/questions.test.ts b/backend/tests/data/questions/questions.test.ts index e8069070..f24601bb 100644 --- a/backend/tests/data/questions/questions.test.ts +++ b/backend/tests/data/questions/questions.test.ts @@ -2,17 +2,18 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { setupTestApp } from '../../setup-tests'; import { QuestionRepository } from '../../../src/data/questions/question-repository'; import { - getAssignmentRepository, getClassRepository, + getAssignmentRepository, + getClassRepository, getGroupRepository, getQuestionRepository, - getStudentRepository + getStudentRepository, } from '../../../src/data/repositories'; import { StudentRepository } from '../../../src/data/users/student-repository'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; import { Language } from '@dwengo-1/common/util/language'; -import {Question} from "../../../src/entities/questions/question.entity"; -import {Class} from "../../../src/entities/classes/class.entity"; -import {Assignment} from "../../../src/entities/assignments/assignment.entity"; +import { Question } from '../../../src/entities/questions/question.entity'; +import { Class } from '../../../src/entities/classes/class.entity'; +import { Assignment } from '../../../src/entities/assignments/assignment.entity'; describe('QuestionRepository', () => { let questionRepository: QuestionRepository; @@ -36,7 +37,7 @@ describe('QuestionRepository', () => { const id = new LearningObjectIdentifier('id03', Language.English, 1); const student = await studentRepository.findByUsername('Noordkaap'); - const clazz = await getClassRepository().findById("id01"); + const clazz = await getClassRepository().findById('id01'); const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 1); const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 1); await questionRepository.createQuestion({ @@ -58,9 +59,9 @@ describe('QuestionRepository', () => { clazz = await getClassRepository().findById('id01'); assignment = await getAssignmentRepository().findByClassAndId(clazz!, 1); loId = { - hruid: "id05", + hruid: 'id05', language: Language.English, - version: 1 + version: 1, }; const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!); sortQuestions(result); @@ -83,8 +84,7 @@ describe('QuestionRepository', () => { }); it("should find only the questions for a certain learning object and assignment asked by the user's group", async () => { - const result = - await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, "Tool"); + const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, 'Tool'); // (student Tool is in group #2) expect(result).toHaveLength(1); @@ -110,8 +110,11 @@ describe('QuestionRepository', () => { function sortQuestions(questions: Question[]): void { questions.sort((a, b) => { - if (a.learningObjectHruid < b.learningObjectHruid) {return -1} - else if (a.learningObjectHruid > b.learningObjectHruid) {return 1} - return a.sequenceNumber! - b.sequenceNumber! + if (a.learningObjectHruid < b.learningObjectHruid) { + return -1; + } else if (a.learningObjectHruid > b.learningObjectHruid) { + return 1; + } + return a.sequenceNumber! - b.sequenceNumber!; }); } diff --git a/backend/tests/services/learning-path/database-learning-path-provider.test.ts b/backend/tests/services/learning-path/database-learning-path-provider.test.ts index 2c7ceb0b..0a0370a3 100644 --- a/backend/tests/services/learning-path/database-learning-path-provider.test.ts +++ b/backend/tests/services/learning-path/database-learning-path-provider.test.ts @@ -25,9 +25,9 @@ import { Student } from '../../../src/entities/users/student.entity.js'; import { LearningObjectNode, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; -const STUDENT_A_USERNAME = "student_a"; -const STUDENT_B_USERNAME = "student_b"; -const CLASS_NAME = "test_class" +const STUDENT_A_USERNAME = 'student_a'; +const STUDENT_B_USERNAME = 'student_b'; +const CLASS_NAME = 'test_class'; async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { const learningObjectRepo = getLearningObjectRepository(); @@ -44,7 +44,7 @@ async function initPersonalizationTestData(): Promise<{ studentA: Student; studentB: Student; }> { - const studentRepo = getStudentRepository() + const studentRepo = getStudentRepository(); const classRepo = getClassRepository(); const assignmentRepo = getAssignmentRepository(); const groupRepo = getGroupRepository(); @@ -75,31 +75,31 @@ async function initPersonalizationTestData(): Promise<{ // Create class for students const testClass = classRepo.create({ classId: CLASS_NAME, - displayName: "Test class" + displayName: 'Test class', }); await classRepo.save(testClass); // Create assignment for students and assign them to different groups const assignment = assignmentRepo.create({ id: 0, - title: "Test assignment", - description: "Test description", + title: 'Test assignment', + description: 'Test description', learningPathHruid: learningContent.learningPath.hruid, learningPathLanguage: learningContent.learningPath.language, - within: testClass - }) + within: testClass, + }); const groupA = groupRepo.create({ groupNumber: 0, members: [studentA], - assignment + assignment, }); await groupRepo.save(groupA); const groupB = groupRepo.create({ groupNumber: 1, members: [studentB], - assignment + assignment, }); await groupRepo.save(groupB); diff --git a/backend/tests/test_assets/questions/questions.testdata.ts b/backend/tests/test_assets/questions/questions.testdata.ts index 1e4a37ef..10a04571 100644 --- a/backend/tests/test_assets/questions/questions.testdata.ts +++ b/backend/tests/test_assets/questions/questions.testdata.ts @@ -2,7 +2,7 @@ import { EntityManager } from '@mikro-orm/core'; import { Question } from '../../../src/entities/questions/question.entity'; import { Language } from '@dwengo-1/common/util/language'; import { Student } from '../../../src/entities/users/student.entity'; -import {Group} from "../../../src/entities/assignments/group.entity"; +import { Group } from '../../../src/entities/assignments/group.entity'; export function makeTestQuestions(em: EntityManager, students: Student[], groups: Group[]): Question[] { const question01 = em.create(Question, { diff --git a/common/src/interfaces/question.ts b/common/src/interfaces/question.ts index 9b00e1bd..bd689c34 100644 --- a/common/src/interfaces/question.ts +++ b/common/src/interfaces/question.ts @@ -1,6 +1,6 @@ import { LearningObjectIdentifier } from './learning-content'; import { StudentDTO } from './student'; -import {GroupDTO} from "./group"; +import { GroupDTO } from './group'; export interface QuestionDTO { learningObjectIdentifier: LearningObjectIdentifier; From 2d55ac6248f48554927f5acefa9d2de008503607 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger <165218235+geraldschmittinger@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:59:25 +0200 Subject: [PATCH 14/71] fix(backend): Foutmelding createQuestionHandler aangepast. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/src/controllers/questions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 26efb4f4..4df165ee 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -144,7 +144,7 @@ export async function createQuestionHandler(req: Request, res: Response): Promis const questionDTO = req.body as QuestionDTO; if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.inGroup || !questionDTO.content) { - res.status(400).json({ error: 'Missing required fields: identifier and content' }); + res.status(400).json({ error: 'Missing required fields: identifier, author, inGroup, and content' }); return; } From 095609d35719c34d77b6b2111efc0ea0095830d8 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 8 Apr 2025 18:08:35 +0200 Subject: [PATCH 15/71] fix: .js toevoegen aan imports --- backend/src/entities/questions/question.entity.ts | 2 +- backend/src/interfaces/group.ts | 4 ++-- backend/src/interfaces/question.ts | 2 +- backend/src/services/questions.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 44ff3e32..44ccfbd3 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -2,7 +2,7 @@ import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; import { Student } from '../users/student.entity.js'; import { QuestionRepository } from '../../data/questions/question-repository.js'; import { Language } from '@dwengo-1/common/util/language'; -import { Group } from '../assignments/group.entity'; +import { Group } from '../assignments/group.entity.js'; @Entity({ repository: () => QuestionRepository }) export class Question { diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts index 295c7e0f..abbd97de 100644 --- a/backend/src/interfaces/group.ts +++ b/backend/src/interfaces/group.ts @@ -2,9 +2,9 @@ import { Group } from '../entities/assignments/group.entity.js'; import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from './assignment.js'; import { mapToStudent, mapToStudentDTO } from './student.js'; import { GroupDTO } from '@dwengo-1/common/interfaces/group'; -import { getGroupRepository } from '../data/repositories'; +import { getGroupRepository } from '../data/repositories.js'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; -import { Class } from '../entities/classes/class.entity'; +import { Class } from '../entities/classes/class.entity.js'; import { StudentDTO } from '@dwengo-1/common/interfaces/student'; export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index a9347047..08887aba 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -2,7 +2,7 @@ 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'; -import { mapToGroupDTOId } from './group'; +import { mapToGroupDTOId } from './group.js'; function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { return { diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index aa3e1298..4794a3dc 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -9,7 +9,7 @@ import { mapToStudent } from '../interfaces/student.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; -import { mapToAssignment } from '../interfaces/assignment'; +import { mapToAssignment } from '../interfaces/assignment.js'; export async function getQuestionsAboutLearningObjectInAssignment( loId: LearningObjectIdentifier, From 41b36f430e6d27a0b96e58aaaa17abb6a2b701f5 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 9 Apr 2025 11:56:02 +0200 Subject: [PATCH 16/71] chore: Maak linting warnings -> errors Closes #180 --- eslint.config.ts | 70 ++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index fb19e5c4..30e8fe2f 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -44,16 +44,16 @@ export default [ // All @typescript-eslint configuration options are listed. // If the rules are commented, they are configured by the inherited configurations. - '@typescript-eslint/adjacent-overload-signatures': 'warn', - '@typescript-eslint/array-type': 'warn', + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/array-type': 'error', '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/ban-ts-comment': ['error', { minimumDescriptionLength: 10 }], '@typescript-eslint/ban-tslint-comment': 'error', camelcase: 'off', - '@typescript-eslint/class-literal-property-style': 'warn', + '@typescript-eslint/class-literal-property-style': 'error', 'class-methods-use-this': 'off', '@typescript-eslint/class-methods-use-this': ['error', { ignoreOverrideMethods: true }], - '@typescript-eslint/consistent-generic-constructors': 'warn', + '@typescript-eslint/consistent-generic-constructors': 'error', '@typescript-eslint/consistent-indexed-object-style': 'error', 'consistent-return': 'off', '@typescript-eslint/consistent-return': 'off', @@ -64,18 +64,18 @@ export default [ 'default-param-last': 'off', '@typescript-eslint/default-param-last': 'error', 'dot-notation': 'off', - '@typescript-eslint/dot-notation': 'warn', - '@typescript-eslint/explicit-function-return-type': 'warn', + '@typescript-eslint/dot-notation': 'error', + '@typescript-eslint/explicit-function-return-type': 'error', '@typescript-eslint/explicit-member-accessibility': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'warn', + '@typescript-eslint/explicit-module-boundary-types': 'error', 'init-declarations': 'off', '@typescript-eslint/init-declarations': 'off', 'max-params': 'off', '@typescript-eslint/max-params': ['error', { max: 6 }], - '@typescript-eslint/member-ordering': 'warn', + '@typescript-eslint/member-ordering': 'error', '@typescript-eslint/method-signature-style': 'off', // Don't care about TypeScript strict mode. '@typescript-eslint/naming-convention': [ - 'warn', + 'error', { // Enforce that all variables, functions and properties are camelCase selector: 'variableLike', @@ -113,7 +113,7 @@ export default [ '@typescript-eslint/no-empty-function': 'error', '@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-empty-object-type': 'error', - '@typescript-eslint/no-explicit-any': 'warn', // Once in production, this should be an error. + '@typescript-eslint/no-explicit-any': 'error', // Once in production, this should be an error. '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', '@typescript-eslint/no-floating-promises': 'error', @@ -121,7 +121,7 @@ export default [ 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', '@typescript-eslint/no-import-type-side-effects': 'error', - '@typescript-eslint/no-inferrable-types': 'warn', + '@typescript-eslint/no-inferrable-types': 'error', 'no-invalid-this': 'off', '@typescript-eslint/no-invalid-this': 'off', '@typescript-eslint/no-invalid-void-type': 'error', @@ -146,10 +146,10 @@ export default [ '@typescript-eslint/no-unsafe-function-type': 'error', 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': 'warn', + '@typescript-eslint/no-unused-expressions': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': [ - 'warn', + 'error', { args: 'all', argsIgnorePattern: '^_', @@ -164,53 +164,53 @@ export default [ '@typescript-eslint/parameter-properties': 'off', - '@typescript-eslint/prefer-find': 'warn', + '@typescript-eslint/prefer-find': 'error', '@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/prefer-readonly-parameter-types': 'off', '@typescript-eslint/prefer-reduce-type-parameter': 'error', - '@typescript-eslint/promise-function-async': 'warn', + '@typescript-eslint/promise-function-async': 'error', - '@typescript-eslint/require-array-sort-compare': 'warn', + '@typescript-eslint/require-array-sort-compare': 'error', - 'no-await-in-loop': 'warn', + 'no-await-in-loop': 'error', 'no-constructor-return': 'error', 'no-inner-declarations': 'error', 'no-self-compare': 'error', 'no-template-curly-in-string': 'error', - 'no-unmodified-loop-condition': 'warn', - 'no-unreachable-loop': 'warn', + 'no-unmodified-loop-condition': 'error', + 'no-unreachable-loop': 'error', 'no-useless-assignment': 'error', - 'arrow-body-style': ['warn', 'as-needed'], - 'block-scoped-var': 'warn', - 'capitalized-comments': 'warn', + 'arrow-body-style': ['error', 'as-needed'], + 'block-scoped-var': 'error', + 'capitalized-comments': 'error', 'consistent-this': 'error', curly: 'error', 'default-case': 'error', 'default-case-last': 'error', eqeqeq: 'error', - 'func-names': 'warn', - 'func-style': ['warn', 'declaration'], - 'grouped-accessor-pairs': ['warn', 'getBeforeSet'], - 'guard-for-in': 'warn', - 'logical-assignment-operators': 'warn', - 'max-classes-per-file': 'warn', + 'func-names': 'error', + 'func-style': ['error', 'declaration'], + 'grouped-accessor-pairs': ['error', 'getBeforeSet'], + 'guard-for-in': 'error', + 'logical-assignment-operators': 'error', + 'max-classes-per-file': 'error', 'no-alert': 'error', - 'no-bitwise': 'warn', - 'no-console': 'warn', - 'no-continue': 'warn', - 'no-else-return': 'warn', + 'no-bitwise': 'error', + 'no-console': 'error', + 'no-continue': 'error', + 'no-else-return': 'error', 'no-eq-null': 'error', 'no-eval': 'error', 'no-extend-native': 'error', 'no-extra-label': 'error', - 'no-implicit-coercion': 'warn', + 'no-implicit-coercion': 'error', 'no-iterator': 'error', - 'no-label-var': 'warn', - 'no-labels': 'warn', + 'no-label-var': 'error', + 'no-labels': 'error', 'no-multi-assign': 'error', 'no-nested-ternary': 'error', 'no-object-constructor': 'error', From 6ab9f8d8a1e002ba72df829b3fa5d4617812b533 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 9 Apr 2025 12:00:44 +0200 Subject: [PATCH 17/71] chore(docs): Staging server toevoegen --- docs/api/generate.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api/generate.ts b/docs/api/generate.ts index 24d3a6cb..2221cb53 100644 --- a/docs/api/generate.ts +++ b/docs/api/generate.ts @@ -15,6 +15,10 @@ const doc = { url: 'http://localhost:3000/', description: 'Development server', }, + { + url: 'http://localhost/', + description: 'Staging server', + }, { url: 'https://sel2-1.ugent.be/', description: 'Production server', From 4e834c9ed1dad5eb25c78fca8e39144d4d2093c5 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 9 Apr 2025 12:03:12 +0200 Subject: [PATCH 18/71] fix(docs): Top level await --- docs/api/generate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/generate.ts b/docs/api/generate.ts index 2221cb53..796369d1 100644 --- a/docs/api/generate.ts +++ b/docs/api/generate.ts @@ -59,4 +59,4 @@ const doc = { const outputFile = './swagger.json'; const routes = ['../../backend/src/app.ts']; -await swaggerAutogen({ openapi: '3.1.0' })(outputFile, routes, doc); +void swaggerAutogen({ openapi: '3.1.0' })(outputFile, routes, doc); From 6574a7eee56bb484e5331b429a1ce66591c6ddde Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 9 Apr 2025 12:29:39 +0200 Subject: [PATCH 19/71] fix(frontend): uuid dependency Closes #182 --- frontend/package.json | 1 + package-lock.json | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index e6ce1426..b6bd5deb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@tanstack/vue-query": "^5.69.0", "axios": "^1.8.2", "oidc-client-ts": "^3.1.0", + "uuid": "^11.1.0", "vue": "^3.5.13", "vue-i18n": "^11.1.2", "vue-router": "^4.5.0", diff --git a/package-lock.json b/package-lock.json index 27d261cb..45c5a310 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,6 +104,7 @@ "@tanstack/vue-query": "^5.69.0", "axios": "^1.8.2", "oidc-client-ts": "^3.1.0", + "uuid": "^11.1.0", "vue": "^3.5.13", "vue-i18n": "^11.1.2", "vue-router": "^4.5.0", From 1c99b03554c02869a5ec69c7645de744fce619ce Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Wed, 9 Apr 2025 19:26:38 +0200 Subject: [PATCH 20/71] feat: class queries useClass en useClasses --- frontend/src/queries/classes.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 frontend/src/queries/classes.ts diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts new file mode 100644 index 00000000..68e07cc0 --- /dev/null +++ b/frontend/src/queries/classes.ts @@ -0,0 +1,30 @@ +import { ClassController, type ClassesResponse, type ClassResponse } from "@/controllers/classes"; +import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { computed, toValue, type MaybeRefOrGetter } from "vue"; + +const classController = new ClassController(); + +function classesQueryKey() { + return ["students"]; +} +function classQueryKey(classid: string) { + return ["student", classid]; +} + + +export function useClassesQuery(full: MaybeRefOrGetter = true): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => (classesQueryKey())), + queryFn: async () => classController.getAll(toValue(full)), + }); +} + +export function useClassQuery( + id: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => classQueryKey(toValue(id)!)), + queryFn: async () => classController.getById(toValue(id)!), + enabled: () => Boolean(toValue(id)), + }); +} \ No newline at end of file From c05ad9a702c61e3fe60011c37095676eec0891a0 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Wed, 9 Apr 2025 19:42:02 +0200 Subject: [PATCH 21/71] feat: class queries students, teachers, teacher invitations GET --- frontend/src/queries/classes.ts | 48 ++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index 68e07cc0..897d7e9e 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -1,16 +1,25 @@ import { ClassController, type ClassesResponse, type ClassResponse } from "@/controllers/classes"; +import type { StudentsResponse } from "@/controllers/students"; import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; const classController = new ClassController(); function classesQueryKey() { - return ["students"]; + return ["classes"]; } function classQueryKey(classid: string) { - return ["student", classid]; + return ["class", classid]; +} +function classStudentsKey(classid: string) { + return ["class-students", classid]; +} +function classTeachersKey(classid: string) { + return ["class-teachers", classid]; +} +function classTeacherInvitationsKey(classid: string) { + return ["class-teacher-invitations", classid]; } - export function useClassesQuery(full: MaybeRefOrGetter = true): UseQueryReturnType { return useQuery({ @@ -27,4 +36,37 @@ export function useClassQuery( queryFn: async () => classController.getById(toValue(id)!), enabled: () => Boolean(toValue(id)), }); +} + +export function useClassStudentsQuery( + id: MaybeRefOrGetter, + full: MaybeRefOrGetter = true +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => classStudentsKey(toValue(id)!)), + queryFn: async () => classController.getStudents(toValue(id)!, toValue(full)!), + enabled: () => Boolean(toValue(id)), + }) +} + +export function useClassTeachersQuery( + id: MaybeRefOrGetter, + full: MaybeRefOrGetter = true +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => classTeachersKey(toValue(id)!)), + queryFn: async () => classController.getTeachers(toValue(id)!, toValue(full)!), + enabled: () => Boolean(toValue(id)), + }); +} + +export function useClassTeacherInvitationsQuery( + id: MaybeRefOrGetter, + full: MaybeRefOrGetter = true +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => classTeacherInvitationsKey(toValue(id)!)), + queryFn: async () => classController.getTeacherInvitations(toValue(id)!, toValue(full)!), + enabled: () => Boolean(toValue(id)), + }); } \ No newline at end of file From a2ae4319577e28e8c9f8c2fad47dafca49c8e346 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Wed, 9 Apr 2025 19:46:05 +0200 Subject: [PATCH 22/71] feat: class queries assignments GET --- frontend/src/queries/classes.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index 897d7e9e..00eb3bf9 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -20,6 +20,9 @@ function classTeachersKey(classid: string) { function classTeacherInvitationsKey(classid: string) { return ["class-teacher-invitations", classid]; } +function classAssignmentsKey(classid: string) { + return ["class-assignments", classid]; +} export function useClassesQuery(full: MaybeRefOrGetter = true): UseQueryReturnType { return useQuery({ @@ -69,4 +72,15 @@ export function useClassTeacherInvitationsQuery( queryFn: async () => classController.getTeacherInvitations(toValue(id)!, toValue(full)!), enabled: () => Boolean(toValue(id)), }); +} + +export function useClassAssignmentsQuery( + id: MaybeRefOrGetter, + full: MaybeRefOrGetter = true +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => classAssignmentsKey(toValue(id)!)), + queryFn: async () => classController.getAssignments(toValue(id)!, toValue(full)!), + enabled: () => Boolean(toValue(id)), + }); } \ No newline at end of file From 45a36a94a58f5df8548a17111e80156f26532f5b Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Wed, 9 Apr 2025 21:16:28 +0200 Subject: [PATCH 23/71] feat: class query get, delete, put geimplementeerd --- frontend/src/queries/classes.ts | 45 ++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index 00eb3bf9..6aca5e7f 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -1,6 +1,7 @@ import { ClassController, type ClassesResponse, type ClassResponse } from "@/controllers/classes"; import type { StudentsResponse } from "@/controllers/students"; -import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; +import { QueryClient, useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; const classController = new ClassController(); @@ -24,6 +25,15 @@ function classAssignmentsKey(classid: string) { return ["class-assignments", classid]; } +async function invalidateAll(classid: string, queryClient: QueryClient): Promise { + await queryClient.invalidateQueries({ queryKey: ["classes"] }); + await queryClient.invalidateQueries({ queryKey: classQueryKey(classid) }); + await queryClient.invalidateQueries({ queryKey: classStudentsKey(classid) }); + await queryClient.invalidateQueries({ queryKey: classTeachersKey(classid) }); + await queryClient.invalidateQueries({ queryKey: classAssignmentsKey(classid) }); + await queryClient.invalidateQueries({ queryKey: classTeacherInvitationsKey(classid) }); +} + export function useClassesQuery(full: MaybeRefOrGetter = true): UseQueryReturnType { return useQuery({ queryKey: computed(() => (classesQueryKey())), @@ -41,6 +51,39 @@ export function useClassQuery( }); } +export function useCreateClassMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data) => classController.createClass(data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["classes"] }); + }, + }); +} + +export function useDeleteClassMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (id) => classController.deleteClass(id), + onSuccess: async (data) => { + await invalidateAll(data.class.id, queryClient); + }, + }); +} + +export function useUpdateClassMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data) => classController.updateClass(data.id, data), + onSuccess: async (data) => { + await invalidateAll(data.class.id, queryClient); + }, + }); +} + export function useClassStudentsQuery( id: MaybeRefOrGetter, full: MaybeRefOrGetter = true From 02076212fee06958bbf2dd650277414ed8789694 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Wed, 9 Apr 2025 21:23:17 +0200 Subject: [PATCH 24/71] feat: class query student en teacher POST --- frontend/src/queries/classes.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index 6aca5e7f..30ca7bee 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -6,6 +6,7 @@ import { computed, toValue, type MaybeRefOrGetter } from "vue"; const classController = new ClassController(); +/* Query cache keys */ function classesQueryKey() { return ["classes"]; } @@ -25,6 +26,7 @@ function classAssignmentsKey(classid: string) { return ["class-assignments", classid]; } +/* Function to invalidate all caches with certain class id */ async function invalidateAll(classid: string, queryClient: QueryClient): Promise { await queryClient.invalidateQueries({ queryKey: ["classes"] }); await queryClient.invalidateQueries({ queryKey: classQueryKey(classid) }); @@ -34,6 +36,7 @@ async function invalidateAll(classid: string, queryClient: QueryClient): Promise await queryClient.invalidateQueries({ queryKey: classTeacherInvitationsKey(classid) }); } +/* Queries */ export function useClassesQuery(full: MaybeRefOrGetter = true): UseQueryReturnType { return useQuery({ queryKey: computed(() => (classesQueryKey())), @@ -95,6 +98,18 @@ export function useClassStudentsQuery( }) } +export function useCreateClassStudentMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ id, username }) => classController.addStudent(id, username), + onSuccess: async (data) => { + await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); + await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id) }); + }, + }); +} + export function useClassTeachersQuery( id: MaybeRefOrGetter, full: MaybeRefOrGetter = true @@ -106,6 +121,18 @@ export function useClassTeachersQuery( }); } +export function useCreateClassTeacherMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ id, username }) => classController.addTeacher(id, username), + onSuccess: async (data) => { + await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); + await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id) }); + }, + }); +} + export function useClassTeacherInvitationsQuery( id: MaybeRefOrGetter, full: MaybeRefOrGetter = true From 77ca390bd2ea1bc5cbeabf2f23df2a3c00164bc2 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Wed, 9 Apr 2025 21:28:04 +0200 Subject: [PATCH 25/71] feat: class queries student & teacher DELETE --- frontend/src/queries/classes.ts | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index 30ca7bee..1ecc0bce 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -98,7 +98,7 @@ export function useClassStudentsQuery( }) } -export function useCreateClassStudentMutation(): UseMutationReturnType { +export function useClassAddStudentMutation(): UseMutationReturnType { const queryClient = useQueryClient(); return useMutation({ @@ -110,6 +110,18 @@ export function useCreateClassStudentMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ id, username }) => classController.deleteStudent(id, username), + onSuccess: async (data) => { + await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); + await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id) }); + }, + }); +} + export function useClassTeachersQuery( id: MaybeRefOrGetter, full: MaybeRefOrGetter = true @@ -121,7 +133,7 @@ export function useClassTeachersQuery( }); } -export function useCreateClassTeacherMutation(): UseMutationReturnType { +export function useClassAddTeacherMutation(): UseMutationReturnType { const queryClient = useQueryClient(); return useMutation({ @@ -133,6 +145,18 @@ export function useCreateClassTeacherMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ id, username }) => classController.deleteTeacher(id, username), + onSuccess: async (data) => { + await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); + await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id) }); + }, + }); +} + export function useClassTeacherInvitationsQuery( id: MaybeRefOrGetter, full: MaybeRefOrGetter = true From 5c9314aa5968a37ecfda9ba868899c65ef87c8c4 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Wed, 9 Apr 2025 21:53:41 +0200 Subject: [PATCH 26/71] feat: group queries voor alle GETs --- frontend/src/controllers/groups.ts | 6 ++- frontend/src/queries/groups.ts | 74 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 frontend/src/queries/groups.ts diff --git a/frontend/src/controllers/groups.ts b/frontend/src/controllers/groups.ts index de6592b5..361dd9d3 100644 --- a/frontend/src/controllers/groups.ts +++ b/frontend/src/controllers/groups.ts @@ -16,11 +16,15 @@ export class GroupController extends BaseController { super(`class/${classid}/assignments/${assignmentNumber}/groups`); } + update(classid: string, assignmentNumber: number) { + this.basePath = `class/${classid}/assignments/${assignmentNumber}/groups`; + } + async getAll(full = true): Promise { return this.get(`/`, { full }); } - async getByNumber(num: number): Promise { + async getByNumber(num: number | string): Promise { return this.get(`/${num}`); } diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts new file mode 100644 index 00000000..6465ce87 --- /dev/null +++ b/frontend/src/queries/groups.ts @@ -0,0 +1,74 @@ +import type { ClassesResponse } from "@/controllers/classes"; +import { GroupController, type GroupResponse, type GroupsResponse } from "@/controllers/groups"; +import type { QuestionsResponse } from "@/controllers/questions"; +import type { SubmissionsResponse } from "@/controllers/submissions"; +import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { computed, toValue, type MaybeRefOrGetter } from "vue"; + +const groupController = new GroupController('', 0); + +function groupsQueryKey(classid: string, assignmentNumber: number) { + return [ "groups", classid, assignmentNumber ]; +} +function groupQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { + return [ "group", classid, assignmentNumber, groupNumber ]; +} +function groupSubmissionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { + return [ "group-submissions", classid, assignmentNumber, groupNumber ]; +} + +export function useGroupsQuery( + classid: string, + assignmentNumber: number, +): UseQueryReturnType { + groupController.update(classid, assignmentNumber); + + return useQuery({ + queryKey: computed(() => (groupsQueryKey(classid, assignmentNumber))), + queryFn: async () => groupController.getAll(), + }); +} + +export function useGroupQuery( + classid: string, + assignmentNumber: number, + groupNumber: MaybeRefOrGetter, +): UseQueryReturnType { + groupController.update(classid, assignmentNumber); + + return useQuery({ + queryKey: computed(() => groupQueryKey(classid, assignmentNumber, toValue(groupNumber)!)), + queryFn: async () => groupController.getByNumber(toValue(groupNumber)!), + enabled: () => !isNaN(Number(toValue(groupNumber))), + }); +} + +export function useGroupSubmissionsQuery( + classid: string, + assignmentNumber: number, + groupNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + groupController.update(classid, assignmentNumber); + + return useQuery({ + queryKey: computed(() => groupSubmissionsQueryKey(classid, assignmentNumber, toValue(groupNumber)!)), + queryFn: async () => groupController.getSubmissions(toValue(groupNumber)!, toValue(full)!), + enabled: () => !isNaN(Number(toValue(groupNumber))), + }); +} + +export function useGroupQuestionsQuery( + classid: string, + assignmentNumber: number, + groupNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + groupController.update(classid, assignmentNumber); + + return useQuery({ + queryKey: computed(() => groupSubmissionsQueryKey(classid, assignmentNumber, toValue(groupNumber)!)), + queryFn: async () => groupController.getSubmissions(toValue(groupNumber)!, toValue(full)!), + enabled: () => !isNaN(Number(toValue(groupNumber))), + }); +} From f8ac37397d63a7d177b8deee897c41b12f5dc7fb Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Wed, 9 Apr 2025 21:56:39 +0200 Subject: [PATCH 27/71] fix: foute key in group query questions gefixt --- frontend/src/queries/groups.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index 6465ce87..a0acd37a 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -16,6 +16,9 @@ function groupQueryKey(classid: string, assignmentNumber: number, groupNumber: n function groupSubmissionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { return [ "group-submissions", classid, assignmentNumber, groupNumber ]; } +function groupQuestionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { + return [ "group-questions", classid, assignmentNumber, groupNumber ]; +} export function useGroupsQuery( classid: string, @@ -67,7 +70,7 @@ export function useGroupQuestionsQuery( groupController.update(classid, assignmentNumber); return useQuery({ - queryKey: computed(() => groupSubmissionsQueryKey(classid, assignmentNumber, toValue(groupNumber)!)), + queryKey: computed(() => groupQuestionsQueryKey(classid, assignmentNumber, toValue(groupNumber)!)), queryFn: async () => groupController.getSubmissions(toValue(groupNumber)!, toValue(full)!), enabled: () => !isNaN(Number(toValue(groupNumber))), }); From d0044fa2195faa5f6ed06e60795950066eb4fc12 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Wed, 9 Apr 2025 22:06:52 +0200 Subject: [PATCH 28/71] fix: group query toValue used in wrong places --- frontend/src/queries/groups.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index a0acd37a..dec35013 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -13,11 +13,11 @@ function groupsQueryKey(classid: string, assignmentNumber: number) { function groupQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { return [ "group", classid, assignmentNumber, groupNumber ]; } -function groupSubmissionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { - return [ "group-submissions", classid, assignmentNumber, groupNumber ]; +function groupSubmissionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number, full: boolean) { + return [ "group-submissions", classid, assignmentNumber, groupNumber, full ]; } -function groupQuestionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { - return [ "group-questions", classid, assignmentNumber, groupNumber ]; +function groupQuestionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number, full: boolean) { + return [ "group-questions", classid, assignmentNumber, groupNumber, full ]; } export function useGroupsQuery( @@ -55,7 +55,7 @@ export function useGroupSubmissionsQuery( groupController.update(classid, assignmentNumber); return useQuery({ - queryKey: computed(() => groupSubmissionsQueryKey(classid, assignmentNumber, toValue(groupNumber)!)), + queryKey: computed(() => groupSubmissionsQueryKey(classid, assignmentNumber, toValue(groupNumber)!, toValue(full)!)), queryFn: async () => groupController.getSubmissions(toValue(groupNumber)!, toValue(full)!), enabled: () => !isNaN(Number(toValue(groupNumber))), }); @@ -67,10 +67,10 @@ export function useGroupQuestionsQuery( groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { - groupController.update(classid, assignmentNumber); + groupController.update(toValue(classid)!, toValue(assignmentNumber)); return useQuery({ - queryKey: computed(() => groupQuestionsQueryKey(classid, assignmentNumber, toValue(groupNumber)!)), + queryKey: computed(() => groupQuestionsQueryKey(classid, assignmentNumber, toValue(groupNumber)!, toValue(full)!)), queryFn: async () => groupController.getSubmissions(toValue(groupNumber)!, toValue(full)!), enabled: () => !isNaN(Number(toValue(groupNumber))), }); From 73e3871af0724aa11265bebabea3bfff533e219d Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Wed, 9 Apr 2025 22:29:08 +0200 Subject: [PATCH 29/71] fix: groups queries types gefixt --- frontend/src/controllers/groups.ts | 36 ++++++++++--------- frontend/src/queries/groups.ts | 56 +++++++++++++++++++----------- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/frontend/src/controllers/groups.ts b/frontend/src/controllers/groups.ts index 361dd9d3..2ba54cb7 100644 --- a/frontend/src/controllers/groups.ts +++ b/frontend/src/controllers/groups.ts @@ -12,39 +12,43 @@ export interface GroupResponse { } export class GroupController extends BaseController { - constructor(classid: string, assignmentNumber: number) { - super(`class/${classid}/assignments/${assignmentNumber}/groups`); + constructor() { + super(''); } update(classid: string, assignmentNumber: number) { this.basePath = `class/${classid}/assignments/${assignmentNumber}/groups`; } - async getAll(full = true): Promise { - return this.get(`/`, { full }); + protected getBasePath(classid: string, assignmentNumber: number) { + return `class/${classid}/assignments/${assignmentNumber}/groups`; } - async getByNumber(num: number | string): Promise { - return this.get(`/${num}`); + async getAll(classid: string, assignmentNumber: number, full = true): Promise { + return this.get(`${this.getBasePath(classid, assignmentNumber)}/`, { full }); } - async createGroup(data: GroupDTO): Promise { - return this.post(`/`, data); + async getByNumber(classid: string, assignmentNumber: number, num: number | string): Promise { + return this.get(`${this.getBasePath(classid, assignmentNumber)}/${num}`); } - async deleteGroup(num: number): Promise { - return this.delete(`/${num}`); + async createGroup(classid: string, assignmentNumber: number, data: GroupDTO): Promise { + return this.post(`${this.getBasePath(classid, assignmentNumber)}/`, data); } - async updateGroup(num: number, data: Partial): Promise { - return this.put(`/${num}`, data); + async deleteGroup(classid: string, assignmentNumber: number, num: number): Promise { + return this.delete(`${this.getBasePath(classid, assignmentNumber)}/${num}`); } - async getSubmissions(groupNumber: number, full = true): Promise { - return this.get(`/${groupNumber}/submissions`, { full }); + async updateGroup(classid: string, assignmentNumber: number, num: number, data: Partial): Promise { + return this.put(`${this.getBasePath(classid, assignmentNumber)}/${num}`, data); } - async getQuestions(groupNumber: number, full = true): Promise { - return this.get(`/${groupNumber}/questions`, { full }); + async getSubmissions(classid: string, assignmentNumber: number, groupNumber: number, full = true): Promise { + return this.get(`${this.getBasePath(classid, assignmentNumber)}/${groupNumber}/submissions`, { full }); + } + + async getQuestions(classid: string, assignmentNumber: number, groupNumber: number, full = true): Promise { + return this.get(`${this.getBasePath(classid, assignmentNumber)}/${groupNumber}/questions`, { full }); } } diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index dec35013..def17524 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -5,7 +5,7 @@ import type { SubmissionsResponse } from "@/controllers/submissions"; import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; -const groupController = new GroupController('', 0); +const groupController = new GroupController(); function groupsQueryKey(classid: string, assignmentNumber: number) { return [ "groups", classid, assignmentNumber ]; @@ -20,15 +20,31 @@ function groupQuestionsQueryKey(classid: string, assignmentNumber: number, group return [ "group-questions", classid, assignmentNumber, groupNumber, full ]; } +function checkEnabled( + classid: string | undefined, + assignmentNumber: number | undefined, + groupNumber: number | undefined, +): boolean { + return Boolean(classid) && !isNaN(Number(groupNumber)) && !isNaN(Number(assignmentNumber)); +} +function toValues( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, +) { + return { cid: toValue(classid), an: toValue(assignmentNumber), gn: toValue(groupNumber) }; +} + export function useGroupsQuery( - classid: string, - assignmentNumber: number, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, ): UseQueryReturnType { - groupController.update(classid, assignmentNumber); + const { cid, an, gn } = toValues(classid, assignmentNumber, 1); return useQuery({ - queryKey: computed(() => (groupsQueryKey(classid, assignmentNumber))), - queryFn: async () => groupController.getAll(), + queryKey: computed(() => (groupsQueryKey(cid!, an!))), + queryFn: async () => groupController.getAll(cid!, an!), + enabled: () => checkEnabled(cid, an, 1), }); } @@ -37,12 +53,12 @@ export function useGroupQuery( assignmentNumber: number, groupNumber: MaybeRefOrGetter, ): UseQueryReturnType { - groupController.update(classid, assignmentNumber); + const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber); return useQuery({ - queryKey: computed(() => groupQueryKey(classid, assignmentNumber, toValue(groupNumber)!)), - queryFn: async () => groupController.getByNumber(toValue(groupNumber)!), - enabled: () => !isNaN(Number(toValue(groupNumber))), + queryKey: computed(() => groupQueryKey(cid!, an!, gn!)), + queryFn: async () => groupController.getByNumber(cid!, an!, gn!), + enabled: () => checkEnabled(cid, an, gn), }); } @@ -52,26 +68,26 @@ export function useGroupSubmissionsQuery( groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { - groupController.update(classid, assignmentNumber); + const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber); return useQuery({ - queryKey: computed(() => groupSubmissionsQueryKey(classid, assignmentNumber, toValue(groupNumber)!, toValue(full)!)), - queryFn: async () => groupController.getSubmissions(toValue(groupNumber)!, toValue(full)!), - enabled: () => !isNaN(Number(toValue(groupNumber))), + queryKey: computed(() => groupSubmissionsQueryKey(cid!, an!, gn!, toValue(full))), + queryFn: async () => groupController.getSubmissions(cid!, an!, gn!, toValue(full)), + enabled: () => checkEnabled(cid, an, gn), }); } export function useGroupQuestionsQuery( - classid: string, - assignmentNumber: number, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { - groupController.update(toValue(classid)!, toValue(assignmentNumber)); + const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber); return useQuery({ - queryKey: computed(() => groupQuestionsQueryKey(classid, assignmentNumber, toValue(groupNumber)!, toValue(full)!)), - queryFn: async () => groupController.getSubmissions(toValue(groupNumber)!, toValue(full)!), - enabled: () => !isNaN(Number(toValue(groupNumber))), + queryKey: computed(() => groupQuestionsQueryKey(cid!, an!, gn!, toValue(full))), + queryFn: async () => groupController.getSubmissions(cid!, an!, gn!, toValue(full)), + enabled: () => checkEnabled(cid, an, gn), }); } From 620a988c6b36ce78581a11ad3cc82b71ca6baa80 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Wed, 9 Apr 2025 23:59:27 +0200 Subject: [PATCH 30/71] fix(backend): Merge-conflicten opgelost & testen gerepareerd. --- backend/src/controllers/submissions.ts | 10 ++++++++-- backend/src/data/questions/question-repository.ts | 4 +++- backend/src/interfaces/submission.ts | 2 +- backend/src/services/submissions.ts | 2 +- backend/tests/data/assignments/assignments.test.ts | 2 +- backend/tests/data/assignments/submissions.test.ts | 2 +- backend/tests/data/questions/questions.test.ts | 4 ++-- common/src/interfaces/question.ts | 6 ++++++ 8 files changed, 23 insertions(+), 9 deletions(-) diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index 4714c1c4..51faa79b 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -15,9 +15,15 @@ import { LearningObjectIdentifier } from '../entities/content/learning-object-id export async function getSubmissionsHandler(req: Request, res: Response): Promise { const loHruid = req.params.hruid; const lang = languageMap[req.query.language as string] || Language.Dutch; - const version = req.query.version || 1; + const version = parseInt(req.query.version as string) ?? 1; - const submissions = await getSubmissionsForLearningObjectAndAssignment(loHruid, lang, version, req.query.classId, req.query.assignmentId); + const submissions = await getSubmissionsForLearningObjectAndAssignment( + loHruid, + lang, + version, + req.query.classId as string, + parseInt(req.query.assignmentId as string) + ); res.json(submissions); } diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index faaf3528..e14c4f14 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -62,7 +62,9 @@ export class QuestionRepository extends DwengoEntityRepository { public async findAllByAssignment(assignment: Assignment): Promise { return this.find({ - author: assignment.groups.flatMap((group) => group.members), + inGroup: { + $contained: assignment.groups + }, learningObjectHruid: assignment.learningPathHruid, learningObjectLanguage: assignment.learningPathLanguage, }); diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts index 085c795d..2aebb0f1 100644 --- a/backend/src/interfaces/submission.ts +++ b/backend/src/interfaces/submission.ts @@ -32,7 +32,7 @@ export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId { }; } -export function mapToSubmission(submissionDTO: SubmissionDTO, submitter: Student, onBehalfOf: Group | undefined): Submission { +export function mapToSubmission(submissionDTO: SubmissionDTO, submitter: Student, onBehalfOf: Group): Submission { return getSubmissionRepository().create({ learningObjectHruid: submissionDTO.learningObjectIdentifier.hruid, learningObjectLanguage: submissionDTO.learningObjectIdentifier.language, diff --git a/backend/src/services/submissions.ts b/backend/src/services/submissions.ts index 76141c4c..0f225845 100644 --- a/backend/src/services/submissions.ts +++ b/backend/src/services/submissions.ts @@ -33,7 +33,7 @@ export async function getAllSubmissions(loId: LearningObjectIdentifier): Promise export async function createSubmission(submissionDTO: SubmissionDTO): Promise { const submitter = await fetchStudent(submissionDTO.submitter.username); - const group = submissionDTO.group ? await getExistingGroupFromGroupDTO(submissionDTO.group) : undefined; + const group = await getExistingGroupFromGroupDTO(submissionDTO.group!); const submissionRepository = getSubmissionRepository(); const submission = mapToSubmission(submissionDTO, submitter, group); diff --git a/backend/tests/data/assignments/assignments.test.ts b/backend/tests/data/assignments/assignments.test.ts index 206ab4fd..1fe52523 100644 --- a/backend/tests/data/assignments/assignments.test.ts +++ b/backend/tests/data/assignments/assignments.test.ts @@ -32,7 +32,7 @@ describe('AssignmentRepository', () => { }); it('should find all by username of the responsible teacher', async () => { - const result = await assignmentRepository.findAllByResponsibleTeacher('FooFighters'); + const result = await assignmentRepository.findAllByResponsibleTeacher('testleerkracht1'); const resultIds = result.map((it) => it.id).sort((a, b) => (a ?? 0) - (b ?? 0)); expect(resultIds).toEqual([1, 3, 4]); diff --git a/backend/tests/data/assignments/submissions.test.ts b/backend/tests/data/assignments/submissions.test.ts index 47e1c414..31aafc1d 100644 --- a/backend/tests/data/assignments/submissions.test.ts +++ b/backend/tests/data/assignments/submissions.test.ts @@ -66,7 +66,7 @@ describe('SubmissionRepository', () => { let assignment: Assignment | null; let loId: LearningObjectIdentifier; it('should find all submissions for a certain learning object and assignment', async () => { - clazz = await classRepository.findById('id01'); + clazz = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); assignment = await assignmentRepository.findByClassAndId(clazz!, 1); loId = { hruid: 'id02', diff --git a/backend/tests/data/questions/questions.test.ts b/backend/tests/data/questions/questions.test.ts index f24601bb..9565e71d 100644 --- a/backend/tests/data/questions/questions.test.ts +++ b/backend/tests/data/questions/questions.test.ts @@ -37,7 +37,7 @@ describe('QuestionRepository', () => { const id = new LearningObjectIdentifier('id03', Language.English, 1); const student = await studentRepository.findByUsername('Noordkaap'); - const clazz = await getClassRepository().findById('id01'); + const clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 1); const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 1); await questionRepository.createQuestion({ @@ -56,7 +56,7 @@ describe('QuestionRepository', () => { let assignment: Assignment | null; let loId: LearningObjectIdentifier; it('should find all questions for a certain learning object and assignment', async () => { - clazz = await getClassRepository().findById('id01'); + clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); assignment = await getAssignmentRepository().findByClassAndId(clazz!, 1); loId = { hruid: 'id05', diff --git a/common/src/interfaces/question.ts b/common/src/interfaces/question.ts index f5eceffd..172d14b7 100644 --- a/common/src/interfaces/question.ts +++ b/common/src/interfaces/question.ts @@ -11,6 +11,12 @@ export interface QuestionDTO { content: string; } +export interface QuestionData { + author?: string; + content: string; + inGroup: GroupDTO; +} + export interface QuestionId { learningObjectIdentifier: LearningObjectIdentifierDTO; sequenceNumber: number; From 8e00fa53f044dd797f0a1f5de5487c6e7a285b7e Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Thu, 10 Apr 2025 00:00:56 +0200 Subject: [PATCH 31/71] style(backend): Linting --- backend/src/services/submissions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/submissions.ts b/backend/src/services/submissions.ts index 0f225845..ec19b280 100644 --- a/backend/src/services/submissions.ts +++ b/backend/src/services/submissions.ts @@ -33,7 +33,7 @@ export async function getAllSubmissions(loId: LearningObjectIdentifier): Promise export async function createSubmission(submissionDTO: SubmissionDTO): Promise { const submitter = await fetchStudent(submissionDTO.submitter.username); - const group = await getExistingGroupFromGroupDTO(submissionDTO.group!); + const group = await getExistingGroupFromGroupDTO(submissionDTO.group); const submissionRepository = getSubmissionRepository(); const submission = mapToSubmission(submissionDTO, submitter, group); From 61424e1ea247e28850dd4bf2d5468a1d5c1e4f89 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Wed, 9 Apr 2025 22:01:34 +0000 Subject: [PATCH 32/71] style: fix linting issues met Prettier --- backend/src/controllers/questions.ts | 4 ++-- backend/src/controllers/submissions.ts | 3 +-- backend/src/data/questions/question-repository.ts | 2 +- backend/src/interfaces/group.ts | 2 +- backend/src/interfaces/submission.ts | 6 +++--- backend/src/services/questions.ts | 8 ++++---- backend/src/services/submissions.ts | 2 +- 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 3fdfdacd..3240aa71 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -5,11 +5,11 @@ import { getAllQuestions, getQuestion, getQuestionsAboutLearningObjectInAssignment, - updateQuestion + updateQuestion, } from '../services/questions.js'; import { FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM } from '../config.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import {QuestionData, QuestionDTO, QuestionId} from '@dwengo-1/common/interfaces/question'; +import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { Language } from '@dwengo-1/common/util/language'; import { requireFields } from './error-helper.js'; diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index 51faa79b..92cf84c1 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -4,7 +4,7 @@ import { deleteSubmission, getAllSubmissions, getSubmission, - getSubmissionsForLearningObjectAndAssignment + getSubmissionsForLearningObjectAndAssignment, } from '../services/submissions.js'; import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; import { Language, languageMap } from '@dwengo-1/common/util/language'; @@ -28,7 +28,6 @@ export async function getSubmissionsHandler(req: Request, res: Response): Promis res.json(submissions); } - export async function getSubmissionHandler(req: Request, res: Response): Promise { const lohruid = req.params.hruid; const lang = languageMap[req.query.language as string] || Language.Dutch; diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index e14c4f14..362ec4c9 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -63,7 +63,7 @@ export class QuestionRepository extends DwengoEntityRepository { public async findAllByAssignment(assignment: Assignment): Promise { return this.find({ inGroup: { - $contained: assignment.groups + $contained: assignment.groups, }, learningObjectHruid: assignment.learningPathHruid, learningObjectLanguage: assignment.learningPathLanguage, diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts index 924a4586..2a8287a3 100644 --- a/backend/src/interfaces/group.ts +++ b/backend/src/interfaces/group.ts @@ -8,7 +8,7 @@ import { getGroupRepository } from '../data/repositories.js'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { Class } from '../entities/classes/class.entity.js'; import { StudentDTO } from '@dwengo-1/common/interfaces/student'; -import {mapToClassDTO} from "./class"; +import { mapToClassDTO } from './class'; export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { const assignmentDto = groupDto.assignment as AssignmentDTO; diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts index 2aebb0f1..aa88f4a1 100644 --- a/backend/src/interfaces/submission.ts +++ b/backend/src/interfaces/submission.ts @@ -2,9 +2,9 @@ import { Submission } from '../entities/assignments/submission.entity.js'; import { mapToGroupDTO } from './group.js'; import { mapToStudentDTO } from './student.js'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; -import {getSubmissionRepository} from "../data/repositories"; -import {Student} from "../entities/users/student.entity"; -import {Group} from "../entities/assignments/group.entity"; +import { getSubmissionRepository } from '../data/repositories'; +import { Student } from '../entities/users/student.entity'; +import { Group } from '../entities/assignments/group.entity'; export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { return { diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 889c6da6..49bf9e92 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -1,16 +1,16 @@ import { getAnswerRepository, getAssignmentRepository, getClassRepository, getGroupRepository, getQuestionRepository } from '../data/repositories.js'; -import {mapToLearningObjectID, mapToQuestionDTO, mapToQuestionDTOId} from '../interfaces/question.js'; +import { mapToLearningObjectID, mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; import { Question } from '../entities/questions/question.entity.js'; import { Answer } from '../entities/questions/answer.entity.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 {QuestionData, QuestionDTO, QuestionId} from '@dwengo-1/common/interfaces/question'; +import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; import { mapToAssignment } from '../interfaces/assignment.js'; -import {AssignmentDTO} from "@dwengo-1/common/interfaces/assignment"; +import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { fetchStudent } from './students.js'; -import {NotFoundException} from "../exceptions/not-found-exception"; +import { NotFoundException } from '../exceptions/not-found-exception'; import { FALLBACK_VERSION_NUM } from '../config.js'; export async function getQuestionsAboutLearningObjectInAssignment( diff --git a/backend/src/services/submissions.ts b/backend/src/services/submissions.ts index ec19b280..64028a5f 100644 --- a/backend/src/services/submissions.ts +++ b/backend/src/services/submissions.ts @@ -6,7 +6,7 @@ import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; import { fetchStudent } from './students.js'; import { getExistingGroupFromGroupDTO } from './groups.js'; import { Submission } from '../entities/assignments/submission.entity.js'; -import {Language} from "@dwengo-1/common/util/language"; +import { Language } from '@dwengo-1/common/util/language'; export async function fetchSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise { const submissionRepository = getSubmissionRepository(); From 439edc58930200298d9a17ee99098f08c418dfb8 Mon Sep 17 00:00:00 2001 From: laurejablonski Date: Fri, 11 Apr 2025 09:56:08 +0200 Subject: [PATCH 33/71] fix: vergeten parameter en verkeerde types fix --- backend/tool/seed.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/tool/seed.ts b/backend/tool/seed.ts index 33344234..3ded9379 100644 --- a/backend/tool/seed.ts +++ b/backend/tool/seed.ts @@ -14,6 +14,8 @@ import { makeTestQuestions } from '../tests/test_assets/questions/questions.test import { makeTestStudents } from '../tests/test_assets/users/students.testdata.js'; import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js'; import { getLogger, Logger } from '../src/logging/initalize.js'; +import { Collection } from '@mikro-orm/core'; +import { Group } from '../dist/entities/assignments/group.entity.js'; const logger: Logger = getLogger(); @@ -34,8 +36,8 @@ export async function seedDatabase(): Promise { const assignments = makeTestAssignemnts(em, classes); const groups = makeTestGroups(em, students, assignments); - assignments[0].groups = groups.slice(0, 3); - assignments[1].groups = groups.slice(3, 4); + assignments[0].groups = new Collection(groups.slice(0, 3)); + assignments[1].groups = new Collection(groups.slice(3, 4)); const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes); const classJoinRequests = makeTestClassJoinRequests(em, students, classes); @@ -43,7 +45,7 @@ export async function seedDatabase(): Promise { learningObjects[1].attachments = attachments; - const questions = makeTestQuestions(em, students); + const questions = makeTestQuestions(em, students, groups); const answers = makeTestAnswers(em, teachers, questions); const submissions = makeTestSubmissions(em, students, groups); From d1f94588b86598f00be616a7464e5e5fd8fdf3ac Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 12 Apr 2025 11:46:12 +0200 Subject: [PATCH 34/71] chore(docs): Bouw docs automatisch in staging --- backend/.env-old | 21 +++++++++++++++++++++ backend/Dockerfile | 20 +++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 backend/.env-old diff --git a/backend/.env-old b/backend/.env-old new file mode 100644 index 00000000..bedfb0b7 --- /dev/null +++ b/backend/.env-old @@ -0,0 +1,21 @@ +PORT=3000 +DWENGO_DB_HOST=db +DWENGO_DB_PORT=5432 +DWENGO_DB_USERNAME=postgres +DWENGO_DB_PASSWORD=postgres +DWENGO_DB_UPDATE=false + +DWENGO_AUTH_STUDENT_URL=http://localhost/idp/realms/student +DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo +DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs +DWENGO_AUTH_TEACHER_URL=http://localhost/idp/realms/teacher +DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo +DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs + +# Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production! +#DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/,127.0.0.1:80,http://127.0.0.1,http://localhost:80,http://127.0.0.1:80,localhost +DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080 + +# Logging and monitoring + +LOKI_HOST=http://logging:3102 diff --git a/backend/Dockerfile b/backend/Dockerfile index bb3464c3..5f37aba9 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -22,6 +22,24 @@ COPY docs ./docs RUN npm run build +FROM node:22 AS docs-stage + +WORKDIR /app/dwengo + +# Install dependencies + +COPY package*.json ./ +COPY docs/package.json ./docs/ + +RUN npm install --silent + +# Build the docs + +COPY docs ./docs +COPY backend ./backend + +RUN npm run swagger -w docs + FROM node:22 AS production-stage WORKDIR /app/dwengo @@ -34,6 +52,7 @@ COPY ./backend/i18n ./i18n COPY --from=build-stage /app/dwengo/common/dist ./common/dist COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist +COPY --from=docs-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json COPY package*.json ./ COPY backend/package.json ./backend/ @@ -42,7 +61,6 @@ COPY common/package.json ./common/ RUN npm install --silent --only=production -COPY ./docs ./docs COPY ./backend/i18n ./backend/i18n EXPOSE 3000 From e12967dc92a630e041c4a32a4f303da9550a41a0 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 12 Apr 2025 11:47:40 +0200 Subject: [PATCH 35/71] chore(docs): swagger.json automatisch genereren Niet meer inchecken in git --- docs/.gitignore | 1 + docs/api/swagger.json | 1964 ----------------------------------------- package.json | 2 +- 3 files changed, 2 insertions(+), 1965 deletions(-) create mode 100644 docs/.gitignore delete mode 100644 docs/api/swagger.json diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..ca73ebc9 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +api/swagger.json diff --git a/docs/api/swagger.json b/docs/api/swagger.json deleted file mode 100644 index 911839d0..00000000 --- a/docs/api/swagger.json +++ /dev/null @@ -1,1964 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "version": "0.1.0", - "title": "Dwengo-1 Backend API", - "description": "Dwengo-1 Backend API using Express, based on VZW Dwengo", - "license": { - "name": "MIT", - "url": "https://github.com/SELab-2/Dwengo-1/blob/336496ab6352ee3f8bf47490c90b5cf81526cef6/LICENSE" - } - }, - "servers": [ - { - "url": "http://localhost:3000/", - "description": "Development server" - }, - { - "url": "https://sel2-1.ugent.be/", - "description": "Production server" - } - ], - "paths": { - "/api/": { - "get": { - "description": "", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/student/": { - "get": { - "tags": ["Student"], - "description": "", - "parameters": [ - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "Created" - }, - "404": { - "description": "Not Found" - } - } - }, - "post": { - "tags": ["Student"], - "description": "", - "responses": { - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "username": { - "example": "any" - }, - "firstName": { - "example": "any" - }, - "lastName": { - "example": "any" - } - } - } - } - } - } - }, - "delete": { - "tags": ["Student"], - "description": "", - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/student/{username}": { - "delete": { - "tags": ["Student"], - "description": "", - "parameters": [ - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - }, - "404": { - "description": "Not Found" - } - } - }, - "get": { - "tags": ["Student"], - "description": "", - "parameters": [ - { - "name": "username", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/student/{id}/classes": { - "get": { - "tags": ["Student"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/student/{id}/submissions": { - "get": { - "tags": ["Student"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/student/{id}/assignments": { - "get": { - "tags": ["Student"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/student/{id}/groups": { - "get": { - "tags": ["Student"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/student/{id}/questions": { - "get": { - "tags": ["Student"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/group/": { - "get": { - "tags": ["Group"], - "description": "", - "parameters": [ - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - }, - "post": { - "tags": ["Group"], - "description": "", - "responses": { - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/group/{groupid}": { - "get": { - "tags": ["Group"], - "description": "", - "parameters": [ - { - "name": "groupid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/group/{id}/questions": { - "get": { - "tags": ["Group"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/assignment/": { - "get": { - "tags": ["Assignment"], - "description": "", - "parameters": [ - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "post": { - "tags": ["Assignment"], - "description": "", - "responses": { - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - }, - "500": { - "description": "Internal Server Error" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "description": { - "example": "any" - }, - "language": { - "example": "any" - }, - "learningPath": { - "example": "any" - }, - "title": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/assignment/{id}": { - "get": { - "tags": ["Assignment"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/assignment/{id}/submissions": { - "get": { - "tags": ["Assignment"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/assignment/{id}/questions": { - "get": { - "tags": ["Assignment"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/assignment/{assignmentid}/groups/": { - "get": { - "tags": ["Assignment"], - "description": "", - "parameters": [ - { - "name": "assignmentid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - }, - "post": { - "tags": ["Assignment"], - "description": "", - "parameters": [ - { - "name": "assignmentid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/assignment/{assignmentid}/groups/{groupid}": { - "get": { - "tags": ["Assignment"], - "description": "", - "parameters": [ - { - "name": "assignmentid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "groupid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/assignment/{assignmentid}/groups/{id}/questions": { - "get": { - "tags": ["Assignment"], - "description": "", - "parameters": [ - { - "name": "assignmentid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/submission/": { - "get": { - "tags": ["Submission"], - "description": "", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/submission/{id}": { - "post": { - "tags": ["Submission"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - }, - "get": { - "tags": ["Submission"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "language", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "version", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - }, - "404": { - "description": "Not Found" - } - } - }, - "delete": { - "tags": ["Submission"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "language", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "version", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/class/": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "post": { - "tags": ["Class"], - "description": "", - "responses": { - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - }, - "500": { - "description": "Internal Server Error" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "displayName": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/class/{id}": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/class/{id}/teacher-invitations": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/class/{id}/students": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/class/{classid}/assignments/": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "classid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "post": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "classid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - }, - "500": { - "description": "Internal Server Error" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "description": { - "example": "any" - }, - "language": { - "example": "any" - }, - "learningPath": { - "example": "any" - }, - "title": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/class/{classid}/assignments/{id}": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "classid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/class/{classid}/assignments/{id}/submissions": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "classid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/class/{classid}/assignments/{id}/questions": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "classid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/class/{classid}/assignments/{assignmentid}/groups/": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "classid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "assignmentid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - }, - "post": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "classid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "assignmentid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/class/{classid}/assignments/{assignmentid}/groups/{groupid}": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "classid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "assignmentid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "groupid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/class/{classid}/assignments/{assignmentid}/groups/{id}/questions": { - "get": { - "tags": ["Class"], - "description": "", - "parameters": [ - { - "name": "classid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "assignmentid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/question/": { - "get": { - "tags": ["Question"], - "description": "", - "parameters": [ - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - }, - "post": { - "tags": ["Question"], - "description": "", - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "learningObjectIdentifier": { - "example": "any" - }, - "author": { - "example": "any" - }, - "content": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/question/{seq}": { - "delete": { - "tags": ["Question"], - "description": "", - "parameters": [ - { - "name": "seq", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - }, - "get": { - "tags": ["Question"], - "description": "", - "parameters": [ - { - "name": "seq", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/question/answers/{seq}": { - "get": { - "tags": ["Question"], - "description": "", - "parameters": [ - { - "name": "seq", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/auth/config": { - "get": { - "tags": ["Auth"], - "description": "", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/auth/testAuthenticatedOnly": { - "get": { - "tags": ["Auth"], - "description": "", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "student": [] - }, - { - "teacher": [] - } - ] - } - }, - "/api/auth/testStudentsOnly": { - "get": { - "tags": ["Auth"], - "description": "", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "student": [] - } - ] - } - }, - "/api/auth/testTeachersOnly": { - "get": { - "tags": ["Auth"], - "description": "", - "responses": { - "200": { - "description": "OK" - } - }, - "security": [ - { - "teacher": [] - } - ] - } - }, - "/api/theme/": { - "get": { - "tags": ["Theme"], - "description": "", - "parameters": [ - { - "name": "language", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/theme/{theme}": { - "get": { - "tags": ["Theme"], - "description": "", - "parameters": [ - { - "name": "theme", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/learningPath/": { - "get": { - "tags": ["Learning Path"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "theme", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "language", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "forStudent", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "forGroup", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "assignmentNo", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "classId", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/learningObject/": { - "get": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/learningObject/{hruid}": { - "get": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/learningObject/{hruid}/html": { - "get": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/learningObject/{hruid}/html/{attachmentName}": { - "get": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "attachmentName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "default": { - "description": "" - } - } - } - }, - "/api/learningObject/{hruid}/submissions/": { - "get": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/learningObject/{hruid}/submissions/{id}": { - "post": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - }, - "get": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "language", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "version", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - }, - "404": { - "description": "Not Found" - } - } - }, - "delete": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "language", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "version", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/learningObject/{hruid}/{version}/questions/": { - "get": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "version", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - }, - "post": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "version", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "learningObjectIdentifier": { - "example": "any" - }, - "author": { - "example": "any" - }, - "content": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/learningObject/{hruid}/{version}/questions/{seq}": { - "delete": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "version", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "seq", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - }, - "get": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "version", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "seq", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/learningObject/{hruid}/{version}/questions/answers/{seq}": { - "get": { - "tags": ["Learning Object"], - "description": "", - "parameters": [ - { - "name": "hruid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "version", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "seq", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "full", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - } - } - }, - "components": { - "securitySchemes": { - "student": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://sel2-1.ugent.be/idp/realms/student/protocol/openid-connect/auth", - "scopes": { - "openid": "openid", - "profile": "profile", - "email": "email" - } - } - } - }, - "teacher": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://sel2-1.ugent.be/idp/realms/teacher/protocol/openid-connect/auth", - "scopes": { - "openid": "openid", - "profile": "profile", - "email": "email" - } - } - } - } - } - } -} diff --git a/package.json b/package.json index 64cfd665..3d9be4d0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "type": "module", "scripts": { - "prebuild": "npm run clean", + "prebuild": "npm run clean && npm run swagger --workspace=docs", "build": "tsc --build tsconfig.build.json", "clean": "tsc --build tsconfig.build.json --clean", "watch": "tsc --build tsconfig.build.json --watch", From f3c14a175076ad926bdcaedee87122be607f54b8 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 12 Apr 2025 11:55:29 +0200 Subject: [PATCH 36/71] fix(backend): Volgorde docs bouwen --- backend/Dockerfile | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 5f37aba9..1d82a484 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -6,8 +6,9 @@ WORKDIR /app/dwengo COPY package*.json ./ COPY backend/package.json ./backend/ -# Backend depends on common +# Backend depends on common and docs COPY common/package.json ./common/ +COPY docs/package.json ./docs/ RUN npm install --silent @@ -22,24 +23,6 @@ COPY docs ./docs RUN npm run build -FROM node:22 AS docs-stage - -WORKDIR /app/dwengo - -# Install dependencies - -COPY package*.json ./ -COPY docs/package.json ./docs/ - -RUN npm install --silent - -# Build the docs - -COPY docs ./docs -COPY backend ./backend - -RUN npm run swagger -w docs - FROM node:22 AS production-stage WORKDIR /app/dwengo @@ -52,7 +35,7 @@ COPY ./backend/i18n ./i18n COPY --from=build-stage /app/dwengo/common/dist ./common/dist COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist -COPY --from=docs-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json +COPY --from=build-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json COPY package*.json ./ COPY backend/package.json ./backend/ From 311e76149c1f27513bef421f3304e703c51a5092 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sat, 12 Apr 2025 17:55:39 +0200 Subject: [PATCH 37/71] feat: teacher invitation backend --- .../src/controllers/teacher-invitations.ts | 38 ++++++++++ .../classes/teacher-invitation-repository.ts | 7 ++ backend/src/interfaces/teacher-invitation.ts | 9 +++ backend/src/routes/teacher-invitations.ts | 17 +++++ backend/src/routes/teachers.ts | 8 +-- backend/src/services/teacher-invitations.ts | 71 +++++++++++++++++++ common/src/interfaces/teacher-invitation.ts | 6 ++ 7 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 backend/src/controllers/teacher-invitations.ts create mode 100644 backend/src/routes/teacher-invitations.ts create mode 100644 backend/src/services/teacher-invitations.ts diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts new file mode 100644 index 00000000..3292b7bf --- /dev/null +++ b/backend/src/controllers/teacher-invitations.ts @@ -0,0 +1,38 @@ +import { Request, Response } from 'express'; +import {requireFields} from "./error-helper"; +import {createInvitation, deleteInvitationFor, getAllInvitations} from "../services/teacher-invitations"; +import {TeacherInvitationData} from "@dwengo-1/common/interfaces/teacher-invitation"; + +export async function getAllInvitationsHandler(req: Request, res: Response): Promise { + const username = req.params.username; + const by = req.query.by === 'true'; + requireFields({ username }); + + const invitations = getAllInvitations(username, by); + + res.json({ invitations }); +} + +export async function createInvitationHandler(req: Request, res: Response): Promise { + const sender = req.body.sender; + const receiver = req.body.receiver; + const classId = req.body.class; + requireFields({ sender, receiver, classId }); + + const data = req.body as TeacherInvitationData; + const invitation = await createInvitation(data); + + res.json({ invitation }); +} + +export async function deleteInvitationForHandler(req: Request, res: Response): Promise { + const sender = req.params.sender; + const receiver = req.params.receiver; + const classId = req.params.class; + const accepted = req.body.accepted !== 'false'; + requireFields({ sender, receiver, classId }); + + const invitation = deleteInvitationFor(sender, receiver, classId, accepted); + + res.json({ invitation }); +} diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index ce059ca8..5461d29b 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -20,4 +20,11 @@ export class TeacherInvitationRepository extends DwengoEntityRepository { + return this.findOne({ + sender: sender, + receiver: receiver, + class: clazz, + }) + } } diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index d9cb9915..98189938 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -2,6 +2,9 @@ import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity import { mapToClassDTO } from './class.js'; import { mapToUserDTO } from './user.js'; import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; +import {getTeacherInvitationRepository} from "../data/repositories"; +import {Teacher} from "../entities/users/teacher.entity"; +import {Class} from "../entities/classes/class.entity"; export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { return { @@ -18,3 +21,9 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea class: invitation.class.classId!, }; } + +export function mapToInvitation(sender: Teacher, receiver: Teacher, cls: Class): TeacherInvitation { + return getTeacherInvitationRepository().create({ + sender, receiver, class: cls + }); +} diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts new file mode 100644 index 00000000..01a18195 --- /dev/null +++ b/backend/src/routes/teacher-invitations.ts @@ -0,0 +1,17 @@ +import express from "express"; +import { + createInvitationHandler, + deleteInvitationForHandler, + getAllInvitationsHandler +} from "../controllers/teacher-invitations"; + +const router = express.Router({ mergeParams: true }); + +router.get('/:username', getAllInvitationsHandler); + +router.post('/', createInvitationHandler); + +router.delete('/:sender/:receiver/:classId', deleteInvitationForHandler); + + +export default router; diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index a6106a80..801eaee8 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -10,6 +10,8 @@ import { getTeacherStudentHandler, updateStudentJoinRequestHandler, } from '../controllers/teachers.js'; +import invitationRouter from './teacher-invitations.js'; + const router = express.Router(); // Root endpoint used to search objects @@ -32,10 +34,6 @@ 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({ - invitations: ['0'], - }); -}); +router.get('/invitations', invitationRouter); export default router; diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts new file mode 100644 index 00000000..1b9ef179 --- /dev/null +++ b/backend/src/services/teacher-invitations.ts @@ -0,0 +1,71 @@ +import {fetchTeacher} from "./teachers"; +import {getTeacherInvitationRepository} from "../data/repositories"; +import {mapToInvitation, mapToTeacherInvitationDTO} from "../interfaces/teacher-invitation"; +import {addClassTeacher, fetchClass} from "./classes"; +import {TeacherInvitationData, TeacherInvitationDTO} from "@dwengo-1/common/interfaces/teacher-invitation"; +import {ConflictException} from "../exceptions/conflict-exception"; +import {Teacher} from "../entities/users/teacher.entity"; +import {Class} from "../entities/classes/class.entity"; +import {NotFoundException} from "../exceptions/not-found-exception"; +import {TeacherInvitation} from "../entities/classes/teacher-invitation.entity"; + +export async function getAllInvitations(username: string, by: boolean): Promise { + const teacher = await fetchTeacher(username); + const teacherInvitationRepository = getTeacherInvitationRepository(); + + let invitations; + if (by) { + invitations = await teacherInvitationRepository.findAllInvitationsBy(teacher); + } else { + invitations = await teacherInvitationRepository.findAllInvitationsFor(teacher); + } + return invitations.map(mapToTeacherInvitationDTO); +} + +export async function createInvitation(data: TeacherInvitationData): Promise { + const teacherInvitationRepository = getTeacherInvitationRepository(); + const sender = await fetchTeacher(data.sender); + const receiver = await fetchTeacher(data.receiver); + + const cls = await fetchClass(data.class); + + if (!cls.teachers.contains(sender)){ + throw new ConflictException("The teacher sending the invite is not part of the class"); + } + + const newInvitation = mapToInvitation(sender, receiver, cls); + await teacherInvitationRepository.save(newInvitation, {preventOverwrite: true}); + + return mapToTeacherInvitationDTO(newInvitation); +} + +async function fetchInvitation(sender: Teacher, receiver: Teacher, cls: Class): Promise { + const teacherInvitationRepository = getTeacherInvitationRepository(); + const invite = await teacherInvitationRepository.findBy(cls, sender, receiver); + + if (!invite){ + throw new NotFoundException("Teacher invite not found"); + } + + return invite; +} + +export async function deleteInvitationFor(usernameSender: string, usernameReceiver: string, classId: string, accepted: boolean) { + const teacherInvitationRepository = getTeacherInvitationRepository(); + const sender = await fetchTeacher(usernameSender); + const receiver = await fetchTeacher(usernameReceiver); + + const cls = await fetchClass(classId); + + const invitation = await fetchInvitation(sender, receiver, cls); + await teacherInvitationRepository.deleteBy(cls, sender, receiver); + + if (accepted){ + await addClassTeacher(classId, usernameReceiver); + } + + return mapToTeacherInvitationDTO(invitation); +} + + + diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index 13709322..c61f9a6a 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -6,3 +6,9 @@ export interface TeacherInvitationDTO { receiver: string | UserDTO; class: string | ClassDTO; } + +export interface TeacherInvitationData { + sender: string; + receiver: string; + class: string; +} From 363eba57451e9da30001c0f9e76548890db4be3b Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sat, 12 Apr 2025 18:21:43 +0200 Subject: [PATCH 38/71] feat: class query full parameter toegevoegd aan keys --- frontend/src/queries/classes.ts | 53 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index 1ecc0bce..4faf5b19 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -7,22 +7,22 @@ import { computed, toValue, type MaybeRefOrGetter } from "vue"; const classController = new ClassController(); /* Query cache keys */ -function classesQueryKey() { - return ["classes"]; +function classesQueryKey(full: boolean) { + return ["classes", full]; } function classQueryKey(classid: string) { return ["class", classid]; } -function classStudentsKey(classid: string) { - return ["class-students", classid]; +function classStudentsKey(classid: string, full: boolean) { + return ["class-students", classid, full]; } -function classTeachersKey(classid: string) { - return ["class-teachers", classid]; +function classTeachersKey(classid: string, full: boolean) { + return ["class-teachers", classid, full]; } -function classTeacherInvitationsKey(classid: string) { - return ["class-teacher-invitations", classid]; +function classTeacherInvitationsKey(classid: string, full: boolean) { + return ["class-teacher-invitations", classid, full]; } -function classAssignmentsKey(classid: string) { +function classAssignmentsKey(classid: string, full: boolean) { return ["class-assignments", classid]; } @@ -30,16 +30,18 @@ function classAssignmentsKey(classid: string) { async function invalidateAll(classid: string, queryClient: QueryClient): Promise { await queryClient.invalidateQueries({ queryKey: ["classes"] }); await queryClient.invalidateQueries({ queryKey: classQueryKey(classid) }); - await queryClient.invalidateQueries({ queryKey: classStudentsKey(classid) }); - await queryClient.invalidateQueries({ queryKey: classTeachersKey(classid) }); - await queryClient.invalidateQueries({ queryKey: classAssignmentsKey(classid) }); - await queryClient.invalidateQueries({ queryKey: classTeacherInvitationsKey(classid) }); + for (let v of [true, false]) { + await queryClient.invalidateQueries({ queryKey: classStudentsKey(classid, v) }); + await queryClient.invalidateQueries({ queryKey: classTeachersKey(classid, v) }); + await queryClient.invalidateQueries({ queryKey: classAssignmentsKey(classid, v) }); + await queryClient.invalidateQueries({ queryKey: classTeacherInvitationsKey(classid, v) }); + } } /* Queries */ export function useClassesQuery(full: MaybeRefOrGetter = true): UseQueryReturnType { return useQuery({ - queryKey: computed(() => (classesQueryKey())), + queryKey: computed(() => (classesQueryKey(toValue(full)))), queryFn: async () => classController.getAll(toValue(full)), }); } @@ -60,7 +62,8 @@ export function useCreateClassMutation(): UseMutationReturnType classController.createClass(data), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ["classes"] }); + await queryClient.invalidateQueries({ queryKey: classesQueryKey(true) }); + await queryClient.invalidateQueries({ queryKey: classesQueryKey(false) }); }, }); } @@ -92,7 +95,7 @@ export function useClassStudentsQuery( full: MaybeRefOrGetter = true ): UseQueryReturnType { return useQuery({ - queryKey: computed(() => classStudentsKey(toValue(id)!)), + queryKey: computed(() => classStudentsKey(toValue(id)!, toValue(full))), queryFn: async () => classController.getStudents(toValue(id)!, toValue(full)!), enabled: () => Boolean(toValue(id)), }) @@ -105,7 +108,8 @@ export function useClassAddStudentMutation(): UseMutationReturnType classController.addStudent(id, username), onSuccess: async (data) => { await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); - await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id) }); + await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) }); + await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) }); }, }); } @@ -117,7 +121,8 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType classController.deleteStudent(id, username), onSuccess: async (data) => { await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); - await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id) }); + await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) }); + await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) }); }, }); } @@ -127,7 +132,7 @@ export function useClassTeachersQuery( full: MaybeRefOrGetter = true ): UseQueryReturnType { return useQuery({ - queryKey: computed(() => classTeachersKey(toValue(id)!)), + queryKey: computed(() => classTeachersKey(toValue(id)!, toValue(full))), queryFn: async () => classController.getTeachers(toValue(id)!, toValue(full)!), enabled: () => Boolean(toValue(id)), }); @@ -140,7 +145,8 @@ export function useClassAddTeacherMutation(): UseMutationReturnType classController.addTeacher(id, username), onSuccess: async (data) => { await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); - await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id) }); + await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id, true) }); + await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id, false) }); }, }); } @@ -152,7 +158,8 @@ export function useClassDeleteTeacherMutation(): UseMutationReturnType classController.deleteTeacher(id, username), onSuccess: async (data) => { await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); - await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id) }); + await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id, true) }); + await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id, false) }); }, }); } @@ -162,7 +169,7 @@ export function useClassTeacherInvitationsQuery( full: MaybeRefOrGetter = true ): UseQueryReturnType { return useQuery({ - queryKey: computed(() => classTeacherInvitationsKey(toValue(id)!)), + queryKey: computed(() => classTeacherInvitationsKey(toValue(id)!, toValue(full))), queryFn: async () => classController.getTeacherInvitations(toValue(id)!, toValue(full)!), enabled: () => Boolean(toValue(id)), }); @@ -173,7 +180,7 @@ export function useClassAssignmentsQuery( full: MaybeRefOrGetter = true ): UseQueryReturnType { return useQuery({ - queryKey: computed(() => classAssignmentsKey(toValue(id)!)), + queryKey: computed(() => classAssignmentsKey(toValue(id)!, toValue(full))), queryFn: async () => classController.getAssignments(toValue(id)!, toValue(full)!), enabled: () => Boolean(toValue(id)), }); From 9bda33123fc311612fc04c5c9402b53855659964 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sat, 12 Apr 2025 18:26:32 +0200 Subject: [PATCH 39/71] feat: group query full parameter toegevoegd aan keys --- frontend/src/queries/groups.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index def17524..5ac9b40b 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -7,8 +7,8 @@ import { computed, toValue, type MaybeRefOrGetter } from "vue"; const groupController = new GroupController(); -function groupsQueryKey(classid: string, assignmentNumber: number) { - return [ "groups", classid, assignmentNumber ]; +function groupsQueryKey(classid: string, assignmentNumber: number, full: boolean) { + return [ "groups", classid, assignmentNumber, full ]; } function groupQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { return [ "group", classid, assignmentNumber, groupNumber ]; @@ -31,18 +31,19 @@ function toValues( classid: MaybeRefOrGetter, assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter, ) { - return { cid: toValue(classid), an: toValue(assignmentNumber), gn: toValue(groupNumber) }; + return { cid: toValue(classid), an: toValue(assignmentNumber), gn: toValue(groupNumber), f: toValue(full) }; } export function useGroupsQuery( classid: MaybeRefOrGetter, assignmentNumber: MaybeRefOrGetter, ): UseQueryReturnType { - const { cid, an, gn } = toValues(classid, assignmentNumber, 1); + const { cid, an, f } = toValues(classid, assignmentNumber, 1, true); return useQuery({ - queryKey: computed(() => (groupsQueryKey(cid!, an!))), + queryKey: computed(() => (groupsQueryKey(cid!, an!, f))), queryFn: async () => groupController.getAll(cid!, an!), enabled: () => checkEnabled(cid, an, 1), }); @@ -53,7 +54,7 @@ export function useGroupQuery( assignmentNumber: number, groupNumber: MaybeRefOrGetter, ): UseQueryReturnType { - const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber); + const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber, true); return useQuery({ queryKey: computed(() => groupQueryKey(cid!, an!, gn!)), @@ -68,11 +69,11 @@ export function useGroupSubmissionsQuery( groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { - const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber); + const { cid, an, gn, f } = toValues(classid, assignmentNumber, groupNumber, full); return useQuery({ - queryKey: computed(() => groupSubmissionsQueryKey(cid!, an!, gn!, toValue(full))), - queryFn: async () => groupController.getSubmissions(cid!, an!, gn!, toValue(full)), + queryKey: computed(() => groupSubmissionsQueryKey(cid!, an!, gn!, f)), + queryFn: async () => groupController.getSubmissions(cid!, an!, gn!, f), enabled: () => checkEnabled(cid, an, gn), }); } @@ -83,11 +84,11 @@ export function useGroupQuestionsQuery( groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { - const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber); + const { cid, an, gn, f } = toValues(classid, assignmentNumber, groupNumber, full); return useQuery({ - queryKey: computed(() => groupQuestionsQueryKey(cid!, an!, gn!, toValue(full))), - queryFn: async () => groupController.getSubmissions(cid!, an!, gn!, toValue(full)), + queryKey: computed(() => groupQuestionsQueryKey(cid!, an!, gn!, f)), + queryFn: async () => groupController.getSubmissions(cid!, an!, gn!, f), enabled: () => checkEnabled(cid, an, gn), }); } From 955be87da31c37dddb61ab081dfa20975c79a3b8 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sat, 12 Apr 2025 19:27:56 +0200 Subject: [PATCH 40/71] feat: group query POST, PUT, DELETE mutations --- frontend/src/queries/groups.ts | 71 +++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index 5ac9b40b..afb1fecb 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -2,7 +2,8 @@ import type { ClassesResponse } from "@/controllers/classes"; import { GroupController, type GroupResponse, type GroupsResponse } from "@/controllers/groups"; import type { QuestionsResponse } from "@/controllers/questions"; import type { SubmissionsResponse } from "@/controllers/submissions"; -import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; +import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; const groupController = new GroupController(); @@ -50,8 +51,8 @@ export function useGroupsQuery( } export function useGroupQuery( - classid: string, - assignmentNumber: number, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, ): UseQueryReturnType { const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber, true); @@ -63,9 +64,69 @@ export function useGroupQuery( }); } +// TODO: find way to check if cid and an are not undefined. +// depends on how this function is used. +export function useCreateClassMutation( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, +): UseMutationReturnType { + const queryClient = useQueryClient(); + const { cid, an } = toValues(classid, assignmentNumber, 1, true); + + return useMutation({ + mutationFn: async (data) => groupController.createGroup(cid!, an!, data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); + }, + }); +} + +export function useDeleteClassMutation( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, +): UseMutationReturnType { + const queryClient = useQueryClient(); + const { cid, an, gn } = toValues(classid, assignmentNumber, 1, true); + + return useMutation({ + mutationFn: async (id) => groupController.deleteGroup(cid!, an!, id), + onSuccess: async (_) => { + await queryClient.invalidateQueries({ queryKey: groupQueryKey(cid!, an!, gn!) }); + + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); + + await queryClient.invalidateQueries({ queryKey: groupSubmissionsQueryKey(cid!, an!, gn!, true) }); + await queryClient.invalidateQueries({ queryKey: groupSubmissionsQueryKey(cid!, an!, gn!, false) }); + + await queryClient.invalidateQueries({ queryKey: groupQuestionsQueryKey(cid!, an!, gn!, true) }); + await queryClient.invalidateQueries({ queryKey: groupQuestionsQueryKey(cid!, an!, gn!, false) }); + }, + }); +} + +export function useUpdateClassMutation( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, +): UseMutationReturnType { + const queryClient = useQueryClient(); + const { cid, an, gn } = toValues(classid, assignmentNumber, 1, true); + + return useMutation({ + mutationFn: async (data) => groupController.updateGroup(cid!, an!, gn!, data), + onSuccess: async (data) => { + await queryClient.invalidateQueries({ queryKey: groupQueryKey(cid!, an!, gn!) }); + + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); + }, + }); +} + export function useGroupSubmissionsQuery( - classid: string, - assignmentNumber: number, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { From 2cda69ef5ef4f79e0d4aeb9b6eed9b10e8bc3c29 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sat, 12 Apr 2025 19:56:04 +0200 Subject: [PATCH 41/71] feat: submission query get all en get --- frontend/src/controllers/submissions.ts | 8 ++- frontend/src/queries/submissions.ts | 66 +++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 frontend/src/queries/submissions.ts diff --git a/frontend/src/controllers/submissions.ts b/frontend/src/controllers/submissions.ts index 837d356c..1f281e62 100644 --- a/frontend/src/controllers/submissions.ts +++ b/frontend/src/controllers/submissions.ts @@ -11,7 +11,11 @@ export interface SubmissionResponse { export class SubmissionController extends BaseController { constructor(classid: string, assignmentNumber: number, groupNumber: number) { - super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}`); + super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}/submissions`); + } + + protected getBasePath(classid: string, assignmentNumber: number, groupNumber: number) { + return `class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}/submissions`; } async getAll(full = true): Promise { @@ -22,7 +26,7 @@ export class SubmissionController extends BaseController { return this.get(`/${submissionNumber}`); } - async createSubmission(data: unknown): Promise { + async createSubmission(data: SubmissionDTO): Promise { return this.post(`/`, data); } diff --git a/frontend/src/queries/submissions.ts b/frontend/src/queries/submissions.ts new file mode 100644 index 00000000..defa6c38 --- /dev/null +++ b/frontend/src/queries/submissions.ts @@ -0,0 +1,66 @@ +import { SubmissionController, type SubmissionResponse, type SubmissionsResponse } from "@/controllers/submissions"; +import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { computed, toValue, type MaybeRefOrGetter } from "vue"; + +function submissionsQueryKey(classid: string, assignmentNumber: number, full: boolean) { + return [ "submissions", classid, assignmentNumber, full ]; +} +function submissionQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { + return [ "submission", classid, assignmentNumber, groupNumber ]; +} + +function checkEnabled( + classid: string | undefined, + assignmentNumber: number | undefined, + groupNumber: number | undefined, + submissionNumber: number | undefined +): boolean { + return Boolean(classid) + && !isNaN(Number(groupNumber)) + && !isNaN(Number(assignmentNumber)) + && !isNaN(Number(submissionNumber)); +} +function toValues( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, + submissionNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter, +) { + return { + cid: toValue(classid), + an: toValue(assignmentNumber), + gn: toValue(groupNumber), + sn: toValue(submissionNumber), + f: toValue(full) + }; +} + +export function useSubmissionsQuery( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, 1, full); + + return useQuery({ + queryKey: computed(() => (submissionsQueryKey(cid!, an!, f))), + queryFn: async () => new SubmissionController(cid!, an!, gn!).getAll(f), + enabled: () => checkEnabled(cid, an, gn, sn), + }); +} + +export function useSubmissionQuery( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, +): UseQueryReturnType { + const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, 1, true); + + return useQuery({ + queryKey: computed(() => submissionQueryKey(cid!, an!, gn!)), + queryFn: async () => new SubmissionController(cid!, an!, gn!).getByNumber(sn!), + enabled: () => checkEnabled(cid, an, gn, sn), + }); +} \ No newline at end of file From 0784db3680d31eda1b52dbe74bd0e810e5ee82c2 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sat, 12 Apr 2025 20:01:53 +0200 Subject: [PATCH 42/71] feat: submission query POST en DELETE --- frontend/src/queries/groups.ts | 8 +++---- frontend/src/queries/submissions.ts | 37 ++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index afb1fecb..8d235d8f 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -66,7 +66,7 @@ export function useGroupQuery( // TODO: find way to check if cid and an are not undefined. // depends on how this function is used. -export function useCreateClassMutation( +export function useCreateGroupMutation( classid: MaybeRefOrGetter, assignmentNumber: MaybeRefOrGetter, ): UseMutationReturnType { @@ -82,7 +82,7 @@ export function useCreateClassMutation( }); } -export function useDeleteClassMutation( +export function useDeleteGroupMutation( classid: MaybeRefOrGetter, assignmentNumber: MaybeRefOrGetter, ): UseMutationReturnType { @@ -91,7 +91,7 @@ export function useDeleteClassMutation( return useMutation({ mutationFn: async (id) => groupController.deleteGroup(cid!, an!, id), - onSuccess: async (_) => { + onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: groupQueryKey(cid!, an!, gn!) }); await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); @@ -106,7 +106,7 @@ export function useDeleteClassMutation( }); } -export function useUpdateClassMutation( +export function useUpdateGroupMutation( classid: MaybeRefOrGetter, assignmentNumber: MaybeRefOrGetter, ): UseMutationReturnType { diff --git a/frontend/src/queries/submissions.ts b/frontend/src/queries/submissions.ts index defa6c38..5c0d74c3 100644 --- a/frontend/src/queries/submissions.ts +++ b/frontend/src/queries/submissions.ts @@ -1,5 +1,6 @@ import { SubmissionController, type SubmissionResponse, type SubmissionsResponse } from "@/controllers/submissions"; -import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission"; +import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; function submissionsQueryKey(classid: string, assignmentNumber: number, full: boolean) { @@ -63,4 +64,38 @@ export function useSubmissionQuery( queryFn: async () => new SubmissionController(cid!, an!, gn!).getByNumber(sn!), enabled: () => checkEnabled(cid, an, gn, sn), }); +} + +export function useCreateSubmissionMutation( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, +): UseMutationReturnType { + const queryClient = useQueryClient(); + const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber, 1, true); + + return useMutation({ + mutationFn: async (data) => new SubmissionController(cid!, an!, gn!).createSubmission(data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, true) }); + await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, false) }); + }, + }); +} + +export function useDeleteGroupMutation( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, +): UseMutationReturnType { + const queryClient = useQueryClient(); + const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber, 1, true); + + return useMutation({ + mutationFn: async (id) => new SubmissionController(cid!, an!, gn!).deleteSubmission(id), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, true) }); + await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, false) }); + }, + }); } \ No newline at end of file From 9c586143829a1c99dc344906d52ab3d29722be0c Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sat, 12 Apr 2025 20:13:14 +0200 Subject: [PATCH 43/71] refactor: group query gerefactord --- frontend/src/controllers/groups.ts | 40 ++++++++++++------------------ frontend/src/queries/groups.ts | 21 ++++++++-------- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/frontend/src/controllers/groups.ts b/frontend/src/controllers/groups.ts index 2ba54cb7..4c38290f 100644 --- a/frontend/src/controllers/groups.ts +++ b/frontend/src/controllers/groups.ts @@ -12,43 +12,35 @@ export interface GroupResponse { } export class GroupController extends BaseController { - constructor() { - super(''); + constructor(classid: string, assignmentNumber: number) { + super(`class/${classid}/assignments/${assignmentNumber}/groups`); } - update(classid: string, assignmentNumber: number) { - this.basePath = `class/${classid}/assignments/${assignmentNumber}/groups`; + async getAll(full = true): Promise { + return this.get(`/`, { full }); } - protected getBasePath(classid: string, assignmentNumber: number) { - return `class/${classid}/assignments/${assignmentNumber}/groups`; + async getByNumber(num: number): Promise { + return this.get(`/${num}`); } - async getAll(classid: string, assignmentNumber: number, full = true): Promise { - return this.get(`${this.getBasePath(classid, assignmentNumber)}/`, { full }); + async createGroup(data: GroupDTO): Promise { + return this.post(`/`, data); } - async getByNumber(classid: string, assignmentNumber: number, num: number | string): Promise { - return this.get(`${this.getBasePath(classid, assignmentNumber)}/${num}`); + async deleteGroup(num: number): Promise { + return this.delete(`/${num}`); } - async createGroup(classid: string, assignmentNumber: number, data: GroupDTO): Promise { - return this.post(`${this.getBasePath(classid, assignmentNumber)}/`, data); + async updateGroup(num: number, data: Partial): Promise { + return this.put(`/${num}`, data); } - async deleteGroup(classid: string, assignmentNumber: number, num: number): Promise { - return this.delete(`${this.getBasePath(classid, assignmentNumber)}/${num}`); + async getSubmissions(num: number, full = true): Promise { + return this.get(`/${num}/submissions`, { full }); } - async updateGroup(classid: string, assignmentNumber: number, num: number, data: Partial): Promise { - return this.put(`${this.getBasePath(classid, assignmentNumber)}/${num}`, data); - } - - async getSubmissions(classid: string, assignmentNumber: number, groupNumber: number, full = true): Promise { - return this.get(`${this.getBasePath(classid, assignmentNumber)}/${groupNumber}/submissions`, { full }); - } - - async getQuestions(classid: string, assignmentNumber: number, groupNumber: number, full = true): Promise { - return this.get(`${this.getBasePath(classid, assignmentNumber)}/${groupNumber}/questions`, { full }); + async getQuestions(num: number, full = true): Promise { + return this.get(`/${num}/questions`, { full }); } } diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index 8d235d8f..7223ebc6 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -6,8 +6,6 @@ import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; -const groupController = new GroupController(); - function groupsQueryKey(classid: string, assignmentNumber: number, full: boolean) { return [ "groups", classid, assignmentNumber, full ]; } @@ -40,12 +38,13 @@ function toValues( export function useGroupsQuery( classid: MaybeRefOrGetter, assignmentNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, ): UseQueryReturnType { - const { cid, an, f } = toValues(classid, assignmentNumber, 1, true); + const { cid, an, f } = toValues(classid, assignmentNumber, 1, full); return useQuery({ queryKey: computed(() => (groupsQueryKey(cid!, an!, f))), - queryFn: async () => groupController.getAll(cid!, an!), + queryFn: async () => new GroupController(cid!, an!).getAll(f), enabled: () => checkEnabled(cid, an, 1), }); } @@ -59,7 +58,7 @@ export function useGroupQuery( return useQuery({ queryKey: computed(() => groupQueryKey(cid!, an!, gn!)), - queryFn: async () => groupController.getByNumber(cid!, an!, gn!), + queryFn: async () => new GroupController(cid!, an!).getByNumber(gn!), enabled: () => checkEnabled(cid, an, gn), }); } @@ -74,7 +73,7 @@ export function useCreateGroupMutation( const { cid, an } = toValues(classid, assignmentNumber, 1, true); return useMutation({ - mutationFn: async (data) => groupController.createGroup(cid!, an!, data), + mutationFn: async (data) => new GroupController(cid!, an!).createGroup(data), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); @@ -90,7 +89,7 @@ export function useDeleteGroupMutation( const { cid, an, gn } = toValues(classid, assignmentNumber, 1, true); return useMutation({ - mutationFn: async (id) => groupController.deleteGroup(cid!, an!, id), + mutationFn: async (id) => new GroupController(cid!, an!).deleteGroup(id), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: groupQueryKey(cid!, an!, gn!) }); @@ -114,8 +113,8 @@ export function useUpdateGroupMutation( const { cid, an, gn } = toValues(classid, assignmentNumber, 1, true); return useMutation({ - mutationFn: async (data) => groupController.updateGroup(cid!, an!, gn!, data), - onSuccess: async (data) => { + mutationFn: async (data) => new GroupController(cid!, an!).updateGroup(gn!, data), + onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: groupQueryKey(cid!, an!, gn!) }); await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); @@ -134,7 +133,7 @@ export function useGroupSubmissionsQuery( return useQuery({ queryKey: computed(() => groupSubmissionsQueryKey(cid!, an!, gn!, f)), - queryFn: async () => groupController.getSubmissions(cid!, an!, gn!, f), + queryFn: async () => new GroupController(cid!, an!).getSubmissions(gn!, f), enabled: () => checkEnabled(cid, an, gn), }); } @@ -149,7 +148,7 @@ export function useGroupQuestionsQuery( return useQuery({ queryKey: computed(() => groupQuestionsQueryKey(cid!, an!, gn!, f)), - queryFn: async () => groupController.getSubmissions(cid!, an!, gn!, f), + queryFn: async () => new GroupController(cid!, an!).getSubmissions(gn!, f), enabled: () => checkEnabled(cid, an, gn), }); } From 802cbfb6e8d5674e86a8b56ccf5c684abbf3bfa7 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sat, 12 Apr 2025 20:14:24 +0200 Subject: [PATCH 44/71] fix: submission query todo toegevoegd --- frontend/src/queries/submissions.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/queries/submissions.ts b/frontend/src/queries/submissions.ts index 5c0d74c3..374d83d6 100644 --- a/frontend/src/queries/submissions.ts +++ b/frontend/src/queries/submissions.ts @@ -66,6 +66,8 @@ export function useSubmissionQuery( }); } +// TODO: find way to check if cid and an are not undefined. +// depends on how this function is used. export function useCreateSubmissionMutation( classid: MaybeRefOrGetter, assignmentNumber: MaybeRefOrGetter, From 3ee66d2f6799290e006c6e92a492791b34e80af0 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sat, 12 Apr 2025 21:26:42 +0200 Subject: [PATCH 45/71] feat: assignment query GET all en GET --- frontend/src/queries/assignments.ts | 54 +++++++++++++++++++++++++++++ frontend/src/queries/submissions.ts | 20 +++++------ 2 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 frontend/src/queries/assignments.ts diff --git a/frontend/src/queries/assignments.ts b/frontend/src/queries/assignments.ts new file mode 100644 index 00000000..a7a8caaf --- /dev/null +++ b/frontend/src/queries/assignments.ts @@ -0,0 +1,54 @@ +import { AssignmentController, type AssignmentsResponse } from "@/controllers/assignments"; +import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { computed, toValue, type MaybeRefOrGetter } from "vue"; + +function assignmentsQueryKey(classid: string, full: boolean) { + return [ "assignments", classid, full ]; +} +function assignmentQueryKey(classid: string, assignmentNumber: number) { + return [ "assignment", classid, assignmentNumber ]; +} + +function checkEnabled( + classid: string | undefined, + assignmentNumber: number | undefined, + groupNumber: number | undefined, +): boolean { + return Boolean(classid) && !isNaN(Number(groupNumber)) && !isNaN(Number(assignmentNumber)); +} +function toValues( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter, +) { + return { cid: toValue(classid), an: toValue(assignmentNumber), gn: toValue(groupNumber), f: toValue(full) }; +} + +export function useAssignmentsQuery( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + const { cid, an, f } = toValues(classid, assignmentNumber, 1, full); + + return useQuery({ + queryKey: computed(() => (assignmentsQueryKey(cid!, f))), + queryFn: async () => new AssignmentController(cid!).getAll(f), + enabled: () => checkEnabled(cid, an, 1), + }); +} + +export function useAssignmentQuery( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, +): UseQueryReturnType { + const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber, true); + + return useQuery({ + queryKey: computed(() => assignmentQueryKey(cid!, an!)), + queryFn: async () => new AssignmentController(cid!).getByNumber(gn!), + enabled: () => checkEnabled(cid, an, gn), + }); +} \ No newline at end of file diff --git a/frontend/src/queries/submissions.ts b/frontend/src/queries/submissions.ts index 374d83d6..3a15f16e 100644 --- a/frontend/src/queries/submissions.ts +++ b/frontend/src/queries/submissions.ts @@ -3,11 +3,11 @@ import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission"; import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; -function submissionsQueryKey(classid: string, assignmentNumber: number, full: boolean) { - return [ "submissions", classid, assignmentNumber, full ]; +function submissionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number, full: boolean) { + return [ "submissions", classid, assignmentNumber, groupNumber, full ]; } -function submissionQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { - return [ "submission", classid, assignmentNumber, groupNumber ]; +function submissionQueryKey(classid: string, assignmentNumber: number, groupNumber: number, submissionNumber: number) { + return [ "submission", classid, assignmentNumber, groupNumber, submissionNumber ]; } function checkEnabled( @@ -46,7 +46,7 @@ export function useSubmissionsQuery( const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, 1, full); return useQuery({ - queryKey: computed(() => (submissionsQueryKey(cid!, an!, f))), + queryKey: computed(() => (submissionsQueryKey(cid!, an!, gn!, f))), queryFn: async () => new SubmissionController(cid!, an!, gn!).getAll(f), enabled: () => checkEnabled(cid, an, gn, sn), }); @@ -60,7 +60,7 @@ export function useSubmissionQuery( const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, 1, true); return useQuery({ - queryKey: computed(() => submissionQueryKey(cid!, an!, gn!)), + queryKey: computed(() => submissionQueryKey(cid!, an!, gn!, sn!)), queryFn: async () => new SubmissionController(cid!, an!, gn!).getByNumber(sn!), enabled: () => checkEnabled(cid, an, gn, sn), }); @@ -79,8 +79,8 @@ export function useCreateSubmissionMutation( return useMutation({ mutationFn: async (data) => new SubmissionController(cid!, an!, gn!).createSubmission(data), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, true) }); - await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, false) }); + await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, gn!, true) }); + await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, gn!, false) }); }, }); } @@ -96,8 +96,8 @@ export function useDeleteGroupMutation( return useMutation({ mutationFn: async (id) => new SubmissionController(cid!, an!, gn!).deleteSubmission(id), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, true) }); - await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, false) }); + await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, gn!, true) }); + await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, gn!, false) }); }, }); } \ No newline at end of file From 5624f3bbfe9b93e05ba08ca0af9d465cab5da501 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sun, 13 Apr 2025 09:48:34 +0200 Subject: [PATCH 46/71] feat: tests --- .../src/controllers/teacher-invitations.ts | 6 +- .../controllers/teacher-invitations.test.ts | 97 +++++++++++++++++++ 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 backend/tests/controllers/teacher-invitations.test.ts diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index 3292b7bf..15bfc936 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -8,7 +8,7 @@ export async function getAllInvitationsHandler(req: Request, res: Response): Pro const by = req.query.by === 'true'; requireFields({ username }); - const invitations = getAllInvitations(username, by); + const invitations = await getAllInvitations(username, by); res.json({ invitations }); } @@ -28,11 +28,11 @@ export async function createInvitationHandler(req: Request, res: Response): Prom export async function deleteInvitationForHandler(req: Request, res: Response): Promise { const sender = req.params.sender; const receiver = req.params.receiver; - const classId = req.params.class; + const classId = req.params.classId; const accepted = req.body.accepted !== 'false'; requireFields({ sender, receiver, classId }); - const invitation = deleteInvitationFor(sender, receiver, classId, accepted); + const invitation = await deleteInvitationFor(sender, receiver, classId, accepted); res.json({ invitation }); } diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts new file mode 100644 index 00000000..8135281a --- /dev/null +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -0,0 +1,97 @@ +import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { Request, Response } from 'express'; +import { setupTestApp } from '../setup-tests.js'; +import { + createInvitationHandler, + deleteInvitationForHandler, + getAllInvitationsHandler +} from "../../src/controllers/teacher-invitations"; +import {TeacherInvitationData} from "@dwengo-1/common/interfaces/teacher-invitation"; +import {getClassHandler} from "../../src/controllers/classes"; + +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 invitations by', async () => { + req = {params: {username: 'LimpBizkit'}, query: {by: 'true' }}; + + await getAllInvitationsHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({invitations: expect.anything()})); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.invitations).to.have.length.greaterThan(0); + }); + + it('Get teacher invitations for', async () => { + req = {params: {username: 'FooFighters'}, query: {by: 'false' }}; + + await getAllInvitationsHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({invitations: expect.anything()})); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.invitations).to.have.length.greaterThan(0); + }); + + it('Create and delete invitation', async () => { + const body = { + sender: 'LimpBizkit', receiver: 'testleerkracht1', + class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + } as TeacherInvitationData; + req = { body }; + + await createInvitationHandler(req as Request, res as Response); + + req = { + params: { + sender: 'LimpBizkit', receiver: 'testleerkracht1', + classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + }, body: { accepted: 'false' } + }; + + await deleteInvitationForHandler(req as Request, res as Response); + }); + + it('Create and accept invitation', async () => { + const body = { + sender: 'LimpBizkit', receiver: 'testleerkracht1', + class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + } as TeacherInvitationData; + req = { body }; + + await createInvitationHandler(req as Request, res as Response); + + req = { + params: { + sender: 'LimpBizkit', receiver: 'testleerkracht1', + classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + }, body: { accepted: 'true' } + }; + + await deleteInvitationForHandler(req as Request, res as Response); + + req = {params: { + id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + }}; + + await getClassHandler(req as Request, res as Response); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.class.teachers).toContain('testleerkracht1'); + }); +}); From 10ec9cbb589d8d0901b9b2355b3fdd8822e39c12 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sun, 13 Apr 2025 11:41:34 +0200 Subject: [PATCH 47/71] feat: assignment query submissions, questions en groups geimplementeerd --- frontend/src/queries/assignments.ts | 55 +++++++++++++++++++++++++++++ frontend/src/queries/groups.ts | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/assignments.ts b/frontend/src/queries/assignments.ts index a7a8caaf..bec4e52a 100644 --- a/frontend/src/queries/assignments.ts +++ b/frontend/src/queries/assignments.ts @@ -1,6 +1,10 @@ import { AssignmentController, type AssignmentsResponse } from "@/controllers/assignments"; +import type { QuestionsResponse } from "@/controllers/questions"; +import type { SubmissionsResponse } from "@/controllers/submissions"; import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; +import { groupsQueryKey } from "./groups"; +import type { GroupsResponse } from "@/controllers/groups"; function assignmentsQueryKey(classid: string, full: boolean) { return [ "assignments", classid, full ]; @@ -8,6 +12,12 @@ function assignmentsQueryKey(classid: string, full: boolean) { function assignmentQueryKey(classid: string, assignmentNumber: number) { return [ "assignment", classid, assignmentNumber ]; } +function assignmentSubmissionsQueryKey(classid: string, assignmentNumber: number, full: boolean) { + return [ "assignment-submissions", classid, assignmentNumber, full ]; +} +function assignmentQuestionsQueryKey(classid: string, assignmentNumber: number, full: boolean) { + return [ "assignment-questions", classid, assignmentNumber, full ]; +} function checkEnabled( classid: string | undefined, @@ -51,4 +61,49 @@ export function useAssignmentQuery( queryFn: async () => new AssignmentController(cid!).getByNumber(gn!), enabled: () => checkEnabled(cid, an, gn), }); +} + +export function useAssignmentSubmissionsQuery( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + const { cid, an, gn, f } = toValues(classid, assignmentNumber, groupNumber, full); + + return useQuery({ + queryKey: computed(() => assignmentSubmissionsQueryKey(cid!, an!, f)), + queryFn: async () => new AssignmentController(cid!).getSubmissions(gn!, f), + enabled: () => checkEnabled(cid, an, gn), + }); +} + +export function useAssignmentQuestionsQuery( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + const { cid, an, gn, f } = toValues(classid, assignmentNumber, groupNumber, full); + + return useQuery({ + queryKey: computed(() => assignmentQuestionsQueryKey(cid!, an!, f)), + queryFn: async () => new AssignmentController(cid!).getQuestions(gn!, f), + enabled: () => checkEnabled(cid, an, gn), + }); +} + +export function useAssignmentGroupsQuery( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + const { cid, an, gn, f } = toValues(classid, assignmentNumber, groupNumber, full); + + return useQuery({ + queryKey: computed(() => groupsQueryKey(cid!, an!, f)), + queryFn: async () => new AssignmentController(cid!).getQuestions(gn!, f), + enabled: () => checkEnabled(cid, an, gn), + }); } \ No newline at end of file diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index 7223ebc6..50a5d92a 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -6,7 +6,7 @@ import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; -function groupsQueryKey(classid: string, assignmentNumber: number, full: boolean) { +export function groupsQueryKey(classid: string, assignmentNumber: number, full: boolean) { return [ "groups", classid, assignmentNumber, full ]; } function groupQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { From 75f1ff013bab439f2cb14205468465686d9bca82 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sun, 13 Apr 2025 12:03:11 +0200 Subject: [PATCH 48/71] feat: assignment query mutation queries geimplementeerd (POST, DELETE, PUT) --- frontend/src/queries/assignments.ts | 79 +++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/frontend/src/queries/assignments.ts b/frontend/src/queries/assignments.ts index bec4e52a..e3bf6d39 100644 --- a/frontend/src/queries/assignments.ts +++ b/frontend/src/queries/assignments.ts @@ -1,10 +1,11 @@ -import { AssignmentController, type AssignmentsResponse } from "@/controllers/assignments"; +import { AssignmentController, type AssignmentResponse, type AssignmentsResponse } from "@/controllers/assignments"; import type { QuestionsResponse } from "@/controllers/questions"; import type { SubmissionsResponse } from "@/controllers/submissions"; -import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; import { groupsQueryKey } from "./groups"; import type { GroupsResponse } from "@/controllers/groups"; +import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; function assignmentsQueryKey(classid: string, full: boolean) { return [ "assignments", classid, full ]; @@ -37,29 +38,89 @@ function toValues( export function useAssignmentsQuery( classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { - const { cid, an, f } = toValues(classid, assignmentNumber, 1, full); + const { cid, f } = toValues(classid, 1, 1, full); return useQuery({ queryKey: computed(() => (assignmentsQueryKey(cid!, f))), queryFn: async () => new AssignmentController(cid!).getAll(f), - enabled: () => checkEnabled(cid, an, 1), + enabled: () => checkEnabled(cid, 1, 1), }); } export function useAssignmentQuery( classid: MaybeRefOrGetter, assignmentNumber: MaybeRefOrGetter, - groupNumber: MaybeRefOrGetter, ): UseQueryReturnType { - const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber, true); + const { cid, an } = toValues(classid, assignmentNumber, 1, true); return useQuery({ queryKey: computed(() => assignmentQueryKey(cid!, an!)), - queryFn: async () => new AssignmentController(cid!).getByNumber(gn!), - enabled: () => checkEnabled(cid, an, gn), + queryFn: async () => new AssignmentController(cid!).getByNumber(an!), + enabled: () => checkEnabled(cid, an, 1), + }); +} + +export function useCreateAssignmentMutation( + classid: MaybeRefOrGetter, +): UseMutationReturnType { + const queryClient = useQueryClient(); + const { cid } = toValues(classid, 1, 1, true); + + return useMutation({ + mutationFn: async (data) => new AssignmentController(cid!).createAssignment(data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, true) }); + await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, false) }); + }, + }); +} + +export function useDeleteAssignmentMutation( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, +): UseMutationReturnType { + const queryClient = useQueryClient(); + const { cid, an } = toValues(classid, assignmentNumber, 1, true); + + return useMutation({ + mutationFn: async (id) => new AssignmentController(cid!).deleteAssignment(id), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: assignmentQueryKey(cid!, an!) }); + + await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, true) }); + await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, false) }); + + await queryClient.invalidateQueries({ queryKey: assignmentSubmissionsQueryKey(cid!, an!, true) }); + await queryClient.invalidateQueries({ queryKey: assignmentSubmissionsQueryKey(cid!, an!, false) }); + + await queryClient.invalidateQueries({ queryKey: assignmentQuestionsQueryKey(cid!, an!, false) }); + await queryClient.invalidateQueries({ queryKey: assignmentQuestionsQueryKey(cid!, an!, true) }); + + // should probably invalidate all groups related to assignment + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); + }, + }); +} + +export function useUpdateAssignmentMutation( + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, +): UseMutationReturnType, unknown> { + const queryClient = useQueryClient(); + const { cid, an } = toValues(classid, assignmentNumber, 1, true); + + return useMutation({ + mutationFn: async (data) => new AssignmentController(cid!).updateAssignment(an!, data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); + + await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, true) }); + await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, false) }); + }, }); } From 7bee08537a67f0a10863d1d21a4b2b807cb999d8 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sun, 13 Apr 2025 14:37:29 +0200 Subject: [PATCH 49/71] fix/refactor: cache keys gefixt, useMutation argumenten rerefactord --- frontend/src/queries/assignments.ts | 77 ++++++++++++++--------------- frontend/src/queries/classes.ts | 38 +++++++++----- frontend/src/queries/groups.ts | 22 ++++++++- frontend/src/queries/submissions.ts | 21 +++++++- 4 files changed, 104 insertions(+), 54 deletions(-) diff --git a/frontend/src/queries/assignments.ts b/frontend/src/queries/assignments.ts index e3bf6d39..ddfe0bc4 100644 --- a/frontend/src/queries/assignments.ts +++ b/frontend/src/queries/assignments.ts @@ -3,9 +3,11 @@ import type { QuestionsResponse } from "@/controllers/questions"; import type { SubmissionsResponse } from "@/controllers/submissions"; import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; -import { groupsQueryKey } from "./groups"; +import { groupsQueryKey, invalidateAllGroupKeys } from "./groups"; import type { GroupsResponse } from "@/controllers/groups"; import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; +import type { QueryClient } from "@tanstack/react-query"; +import { invalidateAllSubmissionKeys } from "./submissions"; function assignmentsQueryKey(classid: string, full: boolean) { return [ "assignments", classid, full ]; @@ -20,6 +22,21 @@ function assignmentQuestionsQueryKey(classid: string, assignmentNumber: number, return [ "assignment-questions", classid, assignmentNumber, full ]; } +export async function invalidateAllAssignmentKeys(queryClient: QueryClient, classid?: string, assignmentNumber?: number) { + const keys = [ + "assignment", + "assignment-submissions", + "assignment-questions", + ]; + + for (let key of keys) { + const queryKey = [key, classid, assignmentNumber].filter(arg => arg !== undefined); + await queryClient.invalidateQueries({ queryKey: queryKey }); + } + + await queryClient.invalidateQueries({ queryKey: [ "assignments", classid ].filter(arg => arg !== undefined) }); +} + function checkEnabled( classid: string | undefined, assignmentNumber: number | undefined, @@ -62,64 +79,44 @@ export function useAssignmentQuery( }); } -export function useCreateAssignmentMutation( - classid: MaybeRefOrGetter, -): UseMutationReturnType { +export function useCreateAssignmentMutation(): UseMutationReturnType { const queryClient = useQueryClient(); - const { cid } = toValues(classid, 1, 1, true); return useMutation({ - mutationFn: async (data) => new AssignmentController(cid!).createAssignment(data), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, true) }); - await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, false) }); + mutationFn: async ({ cid, data }) => new AssignmentController(cid).createAssignment(data), + onSuccess: async (_) => { + await queryClient.invalidateQueries({ queryKey: [ "assignments" ] }); }, }); } -export function useDeleteAssignmentMutation( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, -): UseMutationReturnType { +export function useDeleteAssignmentMutation(): UseMutationReturnType { const queryClient = useQueryClient(); - const { cid, an } = toValues(classid, assignmentNumber, 1, true); return useMutation({ - mutationFn: async (id) => new AssignmentController(cid!).deleteAssignment(id), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: assignmentQueryKey(cid!, an!) }); + mutationFn: async ({ cid, an }) => new AssignmentController(cid).deleteAssignment(an), + onSuccess: async (response) => { + const cid = response.assignment.within; + const an = response.assignment.id; - await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, true) }); - await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, false) }); - - await queryClient.invalidateQueries({ queryKey: assignmentSubmissionsQueryKey(cid!, an!, true) }); - await queryClient.invalidateQueries({ queryKey: assignmentSubmissionsQueryKey(cid!, an!, false) }); - - await queryClient.invalidateQueries({ queryKey: assignmentQuestionsQueryKey(cid!, an!, false) }); - await queryClient.invalidateQueries({ queryKey: assignmentQuestionsQueryKey(cid!, an!, true) }); - - // should probably invalidate all groups related to assignment - await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); - await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); + await invalidateAllAssignmentKeys(queryClient, cid, an); + await invalidateAllGroupKeys(queryClient, cid, an); + await invalidateAllSubmissionKeys(queryClient, cid, an); }, }); } -export function useUpdateAssignmentMutation( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, -): UseMutationReturnType, unknown> { +export function useUpdateAssignmentMutation(): UseMutationReturnType}, unknown> { const queryClient = useQueryClient(); - const { cid, an } = toValues(classid, assignmentNumber, 1, true); return useMutation({ - mutationFn: async (data) => new AssignmentController(cid!).updateAssignment(an!, data), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); - await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); + mutationFn: async ({ cid, an, data }) => new AssignmentController(cid).updateAssignment(an, data), + onSuccess: async (response) => { + const cid = response.assignment.within; + const an = response.assignment.id; - await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, true) }); - await queryClient.invalidateQueries({ queryKey: assignmentsQueryKey(cid!, false) }); + await invalidateAllGroupKeys(queryClient, cid, an); + await queryClient.invalidateQueries({ queryKey: [ "assignments" ] }); }, }); } diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index 4faf5b19..a6d1c157 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -3,6 +3,9 @@ import type { StudentsResponse } from "@/controllers/students"; import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; import { QueryClient, useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; +import { invalidateAllAssignmentKeys } from "./assignments"; +import { invalidateAllGroupKeys } from "./groups"; +import { invalidateAllSubmissionKeys } from "./submissions"; const classController = new ClassController(); @@ -23,19 +26,24 @@ function classTeacherInvitationsKey(classid: string, full: boolean) { return ["class-teacher-invitations", classid, full]; } function classAssignmentsKey(classid: string, full: boolean) { - return ["class-assignments", classid]; + return ["class-assignments", classid, full]; } -/* Function to invalidate all caches with certain class id */ -async function invalidateAll(classid: string, queryClient: QueryClient): Promise { - await queryClient.invalidateQueries({ queryKey: ["classes"] }); - await queryClient.invalidateQueries({ queryKey: classQueryKey(classid) }); - for (let v of [true, false]) { - await queryClient.invalidateQueries({ queryKey: classStudentsKey(classid, v) }); - await queryClient.invalidateQueries({ queryKey: classTeachersKey(classid, v) }); - await queryClient.invalidateQueries({ queryKey: classAssignmentsKey(classid, v) }); - await queryClient.invalidateQueries({ queryKey: classTeacherInvitationsKey(classid, v) }); +export async function invalidateAllClassKeys(queryClient: QueryClient, classid?: string) { + const keys = [ + "class", + "class-students", + "class-teachers", + "class-teacher-invitations", + "class-assignments", + ]; + + for (let key of keys) { + const queryKey = [key, classid].filter(arg => arg !== undefined); + await queryClient.invalidateQueries({ queryKey: queryKey }); } + + await queryClient.invalidateQueries({ queryKey: [ "classes" ] }); } /* Queries */ @@ -74,7 +82,10 @@ export function useDeleteClassMutation(): UseMutationReturnType classController.deleteClass(id), onSuccess: async (data) => { - await invalidateAll(data.class.id, queryClient); + await invalidateAllClassKeys(queryClient, data.class.id); + await invalidateAllAssignmentKeys(queryClient, data.class.id); + await invalidateAllGroupKeys(queryClient, data.class.id); + await invalidateAllSubmissionKeys(queryClient, data.class.id); }, }); } @@ -85,7 +96,10 @@ export function useUpdateClassMutation(): UseMutationReturnType classController.updateClass(data.id, data), onSuccess: async (data) => { - await invalidateAll(data.class.id, queryClient); + await invalidateAllClassKeys(queryClient, data.class.id); + await invalidateAllAssignmentKeys(queryClient, data.class.id); + await invalidateAllGroupKeys(queryClient, data.class.id); + await invalidateAllSubmissionKeys(queryClient, data.class.id); }, }); } diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index 50a5d92a..37270953 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -3,7 +3,7 @@ import { GroupController, type GroupResponse, type GroupsResponse } from "@/cont import type { QuestionsResponse } from "@/controllers/questions"; import type { SubmissionsResponse } from "@/controllers/submissions"; import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; -import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; +import { QueryClient, useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; export function groupsQueryKey(classid: string, assignmentNumber: number, full: boolean) { @@ -19,6 +19,26 @@ function groupQuestionsQueryKey(classid: string, assignmentNumber: number, group return [ "group-questions", classid, assignmentNumber, groupNumber, full ]; } +export async function invalidateAllGroupKeys( + queryClient: QueryClient, + classid?: string, + assignmentNumber?: number, + groupNumber?: number, +) { + const keys = [ + "group", + "group-submissions", + "group-questions", + ]; + + for (let key of keys) { + const queryKey = [key, classid, assignmentNumber, groupNumber].filter(arg => arg !== undefined); + await queryClient.invalidateQueries({ queryKey: queryKey }); + } + + await queryClient.invalidateQueries({ queryKey: [ "groups", classid, assignmentNumber ].filter(arg => arg !== undefined) }); +} + function checkEnabled( classid: string | undefined, assignmentNumber: number | undefined, diff --git a/frontend/src/queries/submissions.ts b/frontend/src/queries/submissions.ts index 3a15f16e..5b8d5691 100644 --- a/frontend/src/queries/submissions.ts +++ b/frontend/src/queries/submissions.ts @@ -1,6 +1,6 @@ import { SubmissionController, type SubmissionResponse, type SubmissionsResponse } from "@/controllers/submissions"; import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission"; -import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; +import { QueryClient, useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; function submissionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number, full: boolean) { @@ -10,6 +10,25 @@ function submissionQueryKey(classid: string, assignmentNumber: number, groupNumb return [ "submission", classid, assignmentNumber, groupNumber, submissionNumber ]; } +export async function invalidateAllSubmissionKeys( + queryClient: QueryClient, + classid?: string, + assignmentNumber?: number, + groupNumber?: number, + submissionNumber?: number, +) { + const keys = [ + "submission", + ]; + + for (let key of keys) { + const queryKey = [key, classid, assignmentNumber, groupNumber, submissionNumber].filter(arg => arg !== undefined); + await queryClient.invalidateQueries({ queryKey: queryKey }); + } + + await queryClient.invalidateQueries({ queryKey: [ "submissions", classid, assignmentNumber, groupNumber ].filter(arg => arg !== undefined) }); +} + function checkEnabled( classid: string | undefined, assignmentNumber: number | undefined, From 389ce91b52e555ec013e4662246d2ceeed394cb0 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sun, 13 Apr 2025 14:47:40 +0200 Subject: [PATCH 50/71] refactor/fix: group query argumenten gerefactord, group query cach keys gefixt --- frontend/src/queries/classes.ts | 7 ++-- frontend/src/queries/groups.ts | 62 +++++++++++++-------------------- 2 files changed, 28 insertions(+), 41 deletions(-) diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index a6d1c157..66be4acf 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -70,8 +70,7 @@ export function useCreateClassMutation(): UseMutationReturnType classController.createClass(data), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: classesQueryKey(true) }); - await queryClient.invalidateQueries({ queryKey: classesQueryKey(false) }); + await queryClient.invalidateQueries({ queryKey: [ "classes" ] }); }, }); } @@ -90,11 +89,11 @@ export function useDeleteClassMutation(): UseMutationReturnType { +export function useUpdateClassMutation(): UseMutationReturnType}, unknown> { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (data) => classController.updateClass(data.id, data), + mutationFn: async ({ cid, data }) => classController.updateClass(cid, data), onSuccess: async (data) => { await invalidateAllClassKeys(queryClient, data.class.id); await invalidateAllAssignmentKeys(queryClient, data.class.id); diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index 37270953..dbe4bcd7 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -5,6 +5,8 @@ import type { SubmissionsResponse } from "@/controllers/submissions"; import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; import { QueryClient, useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; +import { invalidateAllAssignmentKeys } from "./assignments"; +import { invalidateAllSubmissionKeys } from "./submissions"; export function groupsQueryKey(classid: string, assignmentNumber: number, full: boolean) { return [ "groups", classid, assignmentNumber, full ]; @@ -83,62 +85,48 @@ export function useGroupQuery( }); } -// TODO: find way to check if cid and an are not undefined. -// depends on how this function is used. -export function useCreateGroupMutation( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, -): UseMutationReturnType { +export function useCreateGroupMutation(): UseMutationReturnType { const queryClient = useQueryClient(); - const { cid, an } = toValues(classid, assignmentNumber, 1, true); return useMutation({ - mutationFn: async (data) => new GroupController(cid!, an!).createGroup(data), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); - await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); + mutationFn: async ({ cid, an, data }) => new GroupController(cid, an).createGroup(data), + onSuccess: async (response) => { + const cid = typeof(response.group.class) === 'string' ? response.group.class : response.group.class.id; + const an = typeof(response.group.assignment) === 'number' ? response.group.assignment : response.group.assignment.id; + + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid, an, true) }); + await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid, an, false) }); }, }); } -export function useDeleteGroupMutation( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, -): UseMutationReturnType { +export function useDeleteGroupMutation(): UseMutationReturnType { const queryClient = useQueryClient(); - const { cid, an, gn } = toValues(classid, assignmentNumber, 1, true); return useMutation({ - mutationFn: async (id) => new GroupController(cid!, an!).deleteGroup(id), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: groupQueryKey(cid!, an!, gn!) }); + mutationFn: async ({cid, an, gn}) => new GroupController(cid, an).deleteGroup(gn), + onSuccess: async (response) => { + const cid = typeof(response.group.class) === 'string' ? response.group.class : response.group.class.id; + const an = typeof(response.group.assignment) === 'number' ? response.group.assignment : response.group.assignment.id; + const gn = response.group.groupNumber; - await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); - await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); - - await queryClient.invalidateQueries({ queryKey: groupSubmissionsQueryKey(cid!, an!, gn!, true) }); - await queryClient.invalidateQueries({ queryKey: groupSubmissionsQueryKey(cid!, an!, gn!, false) }); - - await queryClient.invalidateQueries({ queryKey: groupQuestionsQueryKey(cid!, an!, gn!, true) }); - await queryClient.invalidateQueries({ queryKey: groupQuestionsQueryKey(cid!, an!, gn!, false) }); + await invalidateAllGroupKeys(queryClient, cid, an, gn); + await invalidateAllSubmissionKeys(queryClient, cid, an, gn); }, }); } -export function useUpdateGroupMutation( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, -): UseMutationReturnType { +export function useUpdateGroupMutation(): UseMutationReturnType}, unknown> { const queryClient = useQueryClient(); - const { cid, an, gn } = toValues(classid, assignmentNumber, 1, true); return useMutation({ - mutationFn: async (data) => new GroupController(cid!, an!).updateGroup(gn!, data), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: groupQueryKey(cid!, an!, gn!) }); + mutationFn: async ({cid, an, gn, data}) => new GroupController(cid, an).updateGroup(gn, data), + onSuccess: async (response) => { + const cid = typeof(response.group.class) === 'string' ? response.group.class : response.group.class.id; + const an = typeof(response.group.assignment) === 'number' ? response.group.assignment : response.group.assignment.id; + const gn = response.group.groupNumber; - await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, true) }); - await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid!, an!, false) }); + await invalidateAllGroupKeys(queryClient, cid, an, gn); }, }); } From 67b60cadedee4a6576e4805c0d78e80e3a195d3c Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sun, 13 Apr 2025 14:55:18 +0200 Subject: [PATCH 51/71] fix/refactor: submission query argumenten gerefactord, cache keys gefixt --- frontend/src/queries/submissions.ts | 54 +++++++++++++++++------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/frontend/src/queries/submissions.ts b/frontend/src/queries/submissions.ts index 5b8d5691..743b1d09 100644 --- a/frontend/src/queries/submissions.ts +++ b/frontend/src/queries/submissions.ts @@ -27,6 +27,8 @@ export async function invalidateAllSubmissionKeys( } await queryClient.invalidateQueries({ queryKey: [ "submissions", classid, assignmentNumber, groupNumber ].filter(arg => arg !== undefined) }); + await queryClient.invalidateQueries({ queryKey: [ "group-submissions", classid, assignmentNumber, groupNumber ].filter(arg => arg !== undefined) }); + await queryClient.invalidateQueries({ queryKey: [ "assignment-submissions", classid, assignmentNumber ].filter(arg => arg !== undefined) }); } function checkEnabled( @@ -85,38 +87,46 @@ export function useSubmissionQuery( }); } -// TODO: find way to check if cid and an are not undefined. -// depends on how this function is used. -export function useCreateSubmissionMutation( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, - groupNumber: MaybeRefOrGetter, -): UseMutationReturnType { +export function useCreateSubmissionMutation(): UseMutationReturnType { const queryClient = useQueryClient(); - const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber, 1, true); return useMutation({ - mutationFn: async (data) => new SubmissionController(cid!, an!, gn!).createSubmission(data), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, gn!, true) }); - await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, gn!, false) }); + mutationFn: async ({cid, an, gn, data}) => new SubmissionController(cid, an, gn).createSubmission(data), + onSuccess: async (response) => { + if (!response.submission.group) { + await invalidateAllSubmissionKeys(queryClient); + } else { + const cls = response.submission.group.class; + const assignment = response.submission.group.assignment; + + const cid = typeof(cls) === 'string' ? cls : cls.id; + const an = typeof(assignment) === 'number' ? assignment : assignment.id; + const gn = response.submission.group.groupNumber; + + await invalidateAllSubmissionKeys(queryClient, cid, an, gn); + } }, }); } -export function useDeleteGroupMutation( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, - groupNumber: MaybeRefOrGetter, -): UseMutationReturnType { +export function useDeleteSubmissionMutation(): UseMutationReturnType { const queryClient = useQueryClient(); - const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber, 1, true); return useMutation({ - mutationFn: async (id) => new SubmissionController(cid!, an!, gn!).deleteSubmission(id), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, gn!, true) }); - await queryClient.invalidateQueries({ queryKey: submissionsQueryKey(cid!, an!, gn!, false) }); + mutationFn: async ({cid, an, gn, sn}) => new SubmissionController(cid, an, gn).deleteSubmission(sn), + onSuccess: async (response) => { + if (!response.submission.group) { + await invalidateAllSubmissionKeys(queryClient); + } else { + const cls = response.submission.group.class; + const assignment = response.submission.group.assignment; + + const cid = typeof(cls) === 'string' ? cls : cls.id; + const an = typeof(assignment) === 'number' ? assignment : assignment.id; + const gn = response.submission.group.groupNumber; + + await invalidateAllSubmissionKeys(queryClient, cid, an, gn); + } }, }); } \ No newline at end of file From 8fde49c05123d7f4e65d067800e61bccdfc784e5 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Sun, 13 Apr 2025 13:00:05 +0000 Subject: [PATCH 52/71] style: fix linting issues met ESLint --- frontend/src/queries/assignments.ts | 2 +- frontend/src/queries/classes.ts | 10 +++++----- frontend/src/queries/groups.ts | 2 +- frontend/src/queries/submissions.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/queries/assignments.ts b/frontend/src/queries/assignments.ts index ddfe0bc4..8dd2912b 100644 --- a/frontend/src/queries/assignments.ts +++ b/frontend/src/queries/assignments.ts @@ -29,7 +29,7 @@ export async function invalidateAllAssignmentKeys(queryClient: QueryClient, clas "assignment-questions", ]; - for (let key of keys) { + for (const key of keys) { const queryKey = [key, classid, assignmentNumber].filter(arg => arg !== undefined); await queryClient.invalidateQueries({ queryKey: queryKey }); } diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index 66be4acf..e4ecfcb1 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -38,7 +38,7 @@ export async function invalidateAllClassKeys(queryClient: QueryClient, classid?: "class-assignments", ]; - for (let key of keys) { + for (const key of keys) { const queryKey = [key, classid].filter(arg => arg !== undefined); await queryClient.invalidateQueries({ queryKey: queryKey }); } @@ -109,7 +109,7 @@ export function useClassStudentsQuery( ): UseQueryReturnType { return useQuery({ queryKey: computed(() => classStudentsKey(toValue(id)!, toValue(full))), - queryFn: async () => classController.getStudents(toValue(id)!, toValue(full)!), + queryFn: async () => classController.getStudents(toValue(id)!, toValue(full)), enabled: () => Boolean(toValue(id)), }) } @@ -146,7 +146,7 @@ export function useClassTeachersQuery( ): UseQueryReturnType { return useQuery({ queryKey: computed(() => classTeachersKey(toValue(id)!, toValue(full))), - queryFn: async () => classController.getTeachers(toValue(id)!, toValue(full)!), + queryFn: async () => classController.getTeachers(toValue(id)!, toValue(full)), enabled: () => Boolean(toValue(id)), }); } @@ -183,7 +183,7 @@ export function useClassTeacherInvitationsQuery( ): UseQueryReturnType { return useQuery({ queryKey: computed(() => classTeacherInvitationsKey(toValue(id)!, toValue(full))), - queryFn: async () => classController.getTeacherInvitations(toValue(id)!, toValue(full)!), + queryFn: async () => classController.getTeacherInvitations(toValue(id)!, toValue(full)), enabled: () => Boolean(toValue(id)), }); } @@ -194,7 +194,7 @@ export function useClassAssignmentsQuery( ): UseQueryReturnType { return useQuery({ queryKey: computed(() => classAssignmentsKey(toValue(id)!, toValue(full))), - queryFn: async () => classController.getAssignments(toValue(id)!, toValue(full)!), + queryFn: async () => classController.getAssignments(toValue(id)!, toValue(full)), enabled: () => Boolean(toValue(id)), }); } \ No newline at end of file diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index dbe4bcd7..7ed67cbc 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -33,7 +33,7 @@ export async function invalidateAllGroupKeys( "group-questions", ]; - for (let key of keys) { + for (const key of keys) { const queryKey = [key, classid, assignmentNumber, groupNumber].filter(arg => arg !== undefined); await queryClient.invalidateQueries({ queryKey: queryKey }); } diff --git a/frontend/src/queries/submissions.ts b/frontend/src/queries/submissions.ts index 743b1d09..5360afaf 100644 --- a/frontend/src/queries/submissions.ts +++ b/frontend/src/queries/submissions.ts @@ -21,7 +21,7 @@ export async function invalidateAllSubmissionKeys( "submission", ]; - for (let key of keys) { + for (const key of keys) { const queryKey = [key, classid, assignmentNumber, groupNumber, submissionNumber].filter(arg => arg !== undefined); await queryClient.invalidateQueries({ queryKey: queryKey }); } From 49c468d431e42e1dcda6f7fc415d5d16f9b5d6cf Mon Sep 17 00:00:00 2001 From: Lint Action Date: Sun, 13 Apr 2025 13:00:10 +0000 Subject: [PATCH 53/71] style: fix linting issues met Prettier --- frontend/src/queries/assignments.ts | 89 +++++++++++++--------- frontend/src/queries/classes.ts | 76 ++++++++++++------- frontend/src/queries/groups.ts | 107 ++++++++++++++++---------- frontend/src/queries/submissions.ts | 113 +++++++++++++++++----------- 4 files changed, 242 insertions(+), 143 deletions(-) diff --git a/frontend/src/queries/assignments.ts b/frontend/src/queries/assignments.ts index 8dd2912b..3251836a 100644 --- a/frontend/src/queries/assignments.ts +++ b/frontend/src/queries/assignments.ts @@ -1,7 +1,13 @@ import { AssignmentController, type AssignmentResponse, type AssignmentsResponse } from "@/controllers/assignments"; import type { QuestionsResponse } from "@/controllers/questions"; import type { SubmissionsResponse } from "@/controllers/submissions"; -import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; +import { + useMutation, + useQuery, + useQueryClient, + type UseMutationReturnType, + type UseQueryReturnType, +} from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; import { groupsQueryKey, invalidateAllGroupKeys } from "./groups"; import type { GroupsResponse } from "@/controllers/groups"; @@ -10,43 +16,43 @@ import type { QueryClient } from "@tanstack/react-query"; import { invalidateAllSubmissionKeys } from "./submissions"; function assignmentsQueryKey(classid: string, full: boolean) { - return [ "assignments", classid, full ]; + return ["assignments", classid, full]; } function assignmentQueryKey(classid: string, assignmentNumber: number) { - return [ "assignment", classid, assignmentNumber ]; + return ["assignment", classid, assignmentNumber]; } function assignmentSubmissionsQueryKey(classid: string, assignmentNumber: number, full: boolean) { - return [ "assignment-submissions", classid, assignmentNumber, full ]; + return ["assignment-submissions", classid, assignmentNumber, full]; } function assignmentQuestionsQueryKey(classid: string, assignmentNumber: number, full: boolean) { - return [ "assignment-questions", classid, assignmentNumber, full ]; + return ["assignment-questions", classid, assignmentNumber, full]; } -export async function invalidateAllAssignmentKeys(queryClient: QueryClient, classid?: string, assignmentNumber?: number) { - const keys = [ - "assignment", - "assignment-submissions", - "assignment-questions", - ]; +export async function invalidateAllAssignmentKeys( + queryClient: QueryClient, + classid?: string, + assignmentNumber?: number, +) { + const keys = ["assignment", "assignment-submissions", "assignment-questions"]; for (const key of keys) { - const queryKey = [key, classid, assignmentNumber].filter(arg => arg !== undefined); + const queryKey = [key, classid, assignmentNumber].filter((arg) => arg !== undefined); await queryClient.invalidateQueries({ queryKey: queryKey }); } - await queryClient.invalidateQueries({ queryKey: [ "assignments", classid ].filter(arg => arg !== undefined) }); + await queryClient.invalidateQueries({ queryKey: ["assignments", classid].filter((arg) => arg !== undefined) }); } function checkEnabled( - classid: string | undefined, - assignmentNumber: number | undefined, + classid: string | undefined, + assignmentNumber: number | undefined, groupNumber: number | undefined, ): boolean { - return Boolean(classid) && !isNaN(Number(groupNumber)) && !isNaN(Number(assignmentNumber)); + return Boolean(classid) && !isNaN(Number(groupNumber)) && !isNaN(Number(assignmentNumber)); } function toValues( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter, ) { @@ -54,21 +60,21 @@ function toValues( } export function useAssignmentsQuery( - classid: MaybeRefOrGetter, + classid: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { const { cid, f } = toValues(classid, 1, 1, full); return useQuery({ - queryKey: computed(() => (assignmentsQueryKey(cid!, f))), + queryKey: computed(() => assignmentsQueryKey(cid!, f)), queryFn: async () => new AssignmentController(cid!).getAll(f), enabled: () => checkEnabled(cid, 1, 1), }); } export function useAssignmentQuery( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, ): UseQueryReturnType { const { cid, an } = toValues(classid, assignmentNumber, 1, true); @@ -79,18 +85,28 @@ export function useAssignmentQuery( }); } -export function useCreateAssignmentMutation(): UseMutationReturnType { +export function useCreateAssignmentMutation(): UseMutationReturnType< + AssignmentResponse, + Error, + { cid: string; data: AssignmentDTO }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ cid, data }) => new AssignmentController(cid).createAssignment(data), onSuccess: async (_) => { - await queryClient.invalidateQueries({ queryKey: [ "assignments" ] }); + await queryClient.invalidateQueries({ queryKey: ["assignments"] }); }, }); } -export function useDeleteAssignmentMutation(): UseMutationReturnType { +export function useDeleteAssignmentMutation(): UseMutationReturnType< + AssignmentResponse, + Error, + { cid: string; an: number }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ @@ -106,7 +122,12 @@ export function useDeleteAssignmentMutation(): UseMutationReturnType}, unknown> { +export function useUpdateAssignmentMutation(): UseMutationReturnType< + AssignmentResponse, + Error, + { cid: string; an: number; data: Partial }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ @@ -116,14 +137,14 @@ export function useUpdateAssignmentMutation(): UseMutationReturnType, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { @@ -137,8 +158,8 @@ export function useAssignmentSubmissionsQuery( } export function useAssignmentQuestionsQuery( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { @@ -152,8 +173,8 @@ export function useAssignmentQuestionsQuery( } export function useAssignmentGroupsQuery( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { @@ -164,4 +185,4 @@ export function useAssignmentGroupsQuery( queryFn: async () => new AssignmentController(cid!).getQuestions(gn!, f), enabled: () => checkEnabled(cid, an, gn), }); -} \ No newline at end of file +} diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index e4ecfcb1..68ba0127 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -1,7 +1,14 @@ import { ClassController, type ClassesResponse, type ClassResponse } from "@/controllers/classes"; import type { StudentsResponse } from "@/controllers/students"; import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; -import { QueryClient, useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; +import { + QueryClient, + useMutation, + useQuery, + useQueryClient, + type UseMutationReturnType, + type UseQueryReturnType, +} from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; import { invalidateAllAssignmentKeys } from "./assignments"; import { invalidateAllGroupKeys } from "./groups"; @@ -30,33 +37,25 @@ function classAssignmentsKey(classid: string, full: boolean) { } export async function invalidateAllClassKeys(queryClient: QueryClient, classid?: string) { - const keys = [ - "class", - "class-students", - "class-teachers", - "class-teacher-invitations", - "class-assignments", - ]; + const keys = ["class", "class-students", "class-teachers", "class-teacher-invitations", "class-assignments"]; for (const key of keys) { - const queryKey = [key, classid].filter(arg => arg !== undefined); + const queryKey = [key, classid].filter((arg) => arg !== undefined); await queryClient.invalidateQueries({ queryKey: queryKey }); } - await queryClient.invalidateQueries({ queryKey: [ "classes" ] }); + await queryClient.invalidateQueries({ queryKey: ["classes"] }); } /* Queries */ export function useClassesQuery(full: MaybeRefOrGetter = true): UseQueryReturnType { return useQuery({ - queryKey: computed(() => (classesQueryKey(toValue(full)))), + queryKey: computed(() => classesQueryKey(toValue(full))), queryFn: async () => classController.getAll(toValue(full)), }); } -export function useClassQuery( - id: MaybeRefOrGetter, -): UseQueryReturnType { +export function useClassQuery(id: MaybeRefOrGetter): UseQueryReturnType { return useQuery({ queryKey: computed(() => classQueryKey(toValue(id)!)), queryFn: async () => classController.getById(toValue(id)!), @@ -70,7 +69,7 @@ export function useCreateClassMutation(): UseMutationReturnType classController.createClass(data), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: [ "classes" ] }); + await queryClient.invalidateQueries({ queryKey: ["classes"] }); }, }); } @@ -89,7 +88,12 @@ export function useDeleteClassMutation(): UseMutationReturnType}, unknown> { +export function useUpdateClassMutation(): UseMutationReturnType< + ClassResponse, + Error, + { cid: string; data: Partial }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ @@ -105,16 +109,21 @@ export function useUpdateClassMutation(): UseMutationReturnType, - full: MaybeRefOrGetter = true + full: MaybeRefOrGetter = true, ): UseQueryReturnType { return useQuery({ queryKey: computed(() => classStudentsKey(toValue(id)!, toValue(full))), queryFn: async () => classController.getStudents(toValue(id)!, toValue(full)), enabled: () => Boolean(toValue(id)), - }) + }); } -export function useClassAddStudentMutation(): UseMutationReturnType { +export function useClassAddStudentMutation(): UseMutationReturnType< + ClassResponse, + Error, + { id: string; username: string }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ @@ -127,7 +136,12 @@ export function useClassAddStudentMutation(): UseMutationReturnType { +export function useClassDeleteStudentMutation(): UseMutationReturnType< + ClassResponse, + Error, + { id: string; username: string }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ @@ -142,7 +156,7 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType, - full: MaybeRefOrGetter = true + full: MaybeRefOrGetter = true, ): UseQueryReturnType { return useQuery({ queryKey: computed(() => classTeachersKey(toValue(id)!, toValue(full))), @@ -151,7 +165,12 @@ export function useClassTeachersQuery( }); } -export function useClassAddTeacherMutation(): UseMutationReturnType { +export function useClassAddTeacherMutation(): UseMutationReturnType< + ClassResponse, + Error, + { id: string; username: string }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ @@ -164,7 +183,12 @@ export function useClassAddTeacherMutation(): UseMutationReturnType { +export function useClassDeleteTeacherMutation(): UseMutationReturnType< + ClassResponse, + Error, + { id: string; username: string }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ @@ -179,7 +203,7 @@ export function useClassDeleteTeacherMutation(): UseMutationReturnType, - full: MaybeRefOrGetter = true + full: MaybeRefOrGetter = true, ): UseQueryReturnType { return useQuery({ queryKey: computed(() => classTeacherInvitationsKey(toValue(id)!, toValue(full))), @@ -190,11 +214,11 @@ export function useClassTeacherInvitationsQuery( export function useClassAssignmentsQuery( id: MaybeRefOrGetter, - full: MaybeRefOrGetter = true + full: MaybeRefOrGetter = true, ): UseQueryReturnType { return useQuery({ queryKey: computed(() => classAssignmentsKey(toValue(id)!, toValue(full))), queryFn: async () => classController.getAssignments(toValue(id)!, toValue(full)), enabled: () => Boolean(toValue(id)), }); -} \ No newline at end of file +} diff --git a/frontend/src/queries/groups.ts b/frontend/src/queries/groups.ts index 7ed67cbc..cdef2899 100644 --- a/frontend/src/queries/groups.ts +++ b/frontend/src/queries/groups.ts @@ -3,54 +3,59 @@ import { GroupController, type GroupResponse, type GroupsResponse } from "@/cont import type { QuestionsResponse } from "@/controllers/questions"; import type { SubmissionsResponse } from "@/controllers/submissions"; import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; -import { QueryClient, useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; +import { + QueryClient, + useMutation, + useQuery, + useQueryClient, + type UseMutationReturnType, + type UseQueryReturnType, +} from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; import { invalidateAllAssignmentKeys } from "./assignments"; import { invalidateAllSubmissionKeys } from "./submissions"; export function groupsQueryKey(classid: string, assignmentNumber: number, full: boolean) { - return [ "groups", classid, assignmentNumber, full ]; + return ["groups", classid, assignmentNumber, full]; } function groupQueryKey(classid: string, assignmentNumber: number, groupNumber: number) { - return [ "group", classid, assignmentNumber, groupNumber ]; + return ["group", classid, assignmentNumber, groupNumber]; } function groupSubmissionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number, full: boolean) { - return [ "group-submissions", classid, assignmentNumber, groupNumber, full ]; + return ["group-submissions", classid, assignmentNumber, groupNumber, full]; } function groupQuestionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number, full: boolean) { - return [ "group-questions", classid, assignmentNumber, groupNumber, full ]; + return ["group-questions", classid, assignmentNumber, groupNumber, full]; } export async function invalidateAllGroupKeys( - queryClient: QueryClient, - classid?: string, + queryClient: QueryClient, + classid?: string, assignmentNumber?: number, groupNumber?: number, ) { - const keys = [ - "group", - "group-submissions", - "group-questions", - ]; + const keys = ["group", "group-submissions", "group-questions"]; for (const key of keys) { - const queryKey = [key, classid, assignmentNumber, groupNumber].filter(arg => arg !== undefined); + const queryKey = [key, classid, assignmentNumber, groupNumber].filter((arg) => arg !== undefined); await queryClient.invalidateQueries({ queryKey: queryKey }); } - await queryClient.invalidateQueries({ queryKey: [ "groups", classid, assignmentNumber ].filter(arg => arg !== undefined) }); + await queryClient.invalidateQueries({ + queryKey: ["groups", classid, assignmentNumber].filter((arg) => arg !== undefined), + }); } function checkEnabled( - classid: string | undefined, - assignmentNumber: number | undefined, + classid: string | undefined, + assignmentNumber: number | undefined, groupNumber: number | undefined, ): boolean { - return Boolean(classid) && !isNaN(Number(groupNumber)) && !isNaN(Number(assignmentNumber)); + return Boolean(classid) && !isNaN(Number(groupNumber)) && !isNaN(Number(assignmentNumber)); } function toValues( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter, ) { @@ -58,22 +63,22 @@ function toValues( } export function useGroupsQuery( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { const { cid, an, f } = toValues(classid, assignmentNumber, 1, full); return useQuery({ - queryKey: computed(() => (groupsQueryKey(cid!, an!, f))), + queryKey: computed(() => groupsQueryKey(cid!, an!, f)), queryFn: async () => new GroupController(cid!, an!).getAll(f), enabled: () => checkEnabled(cid, an, 1), }); } export function useGroupQuery( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, ): UseQueryReturnType { const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber, true); @@ -85,14 +90,22 @@ export function useGroupQuery( }); } -export function useCreateGroupMutation(): UseMutationReturnType { +export function useCreateGroupMutation(): UseMutationReturnType< + GroupResponse, + Error, + { cid: string; an: number; data: GroupDTO }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ cid, an, data }) => new GroupController(cid, an).createGroup(data), onSuccess: async (response) => { - const cid = typeof(response.group.class) === 'string' ? response.group.class : response.group.class.id; - const an = typeof(response.group.assignment) === 'number' ? response.group.assignment : response.group.assignment.id; + const cid = typeof response.group.class === "string" ? response.group.class : response.group.class.id; + const an = + typeof response.group.assignment === "number" + ? response.group.assignment + : response.group.assignment.id; await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid, an, true) }); await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid, an, false) }); @@ -100,14 +113,22 @@ export function useCreateGroupMutation(): UseMutationReturnType { +export function useDeleteGroupMutation(): UseMutationReturnType< + GroupResponse, + Error, + { cid: string; an: number; gn: number }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({cid, an, gn}) => new GroupController(cid, an).deleteGroup(gn), + mutationFn: async ({ cid, an, gn }) => new GroupController(cid, an).deleteGroup(gn), onSuccess: async (response) => { - const cid = typeof(response.group.class) === 'string' ? response.group.class : response.group.class.id; - const an = typeof(response.group.assignment) === 'number' ? response.group.assignment : response.group.assignment.id; + const cid = typeof response.group.class === "string" ? response.group.class : response.group.class.id; + const an = + typeof response.group.assignment === "number" + ? response.group.assignment + : response.group.assignment.id; const gn = response.group.groupNumber; await invalidateAllGroupKeys(queryClient, cid, an, gn); @@ -116,14 +137,22 @@ export function useDeleteGroupMutation(): UseMutationReturnType}, unknown> { +export function useUpdateGroupMutation(): UseMutationReturnType< + GroupResponse, + Error, + { cid: string; an: number; gn: number; data: Partial }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({cid, an, gn, data}) => new GroupController(cid, an).updateGroup(gn, data), + mutationFn: async ({ cid, an, gn, data }) => new GroupController(cid, an).updateGroup(gn, data), onSuccess: async (response) => { - const cid = typeof(response.group.class) === 'string' ? response.group.class : response.group.class.id; - const an = typeof(response.group.assignment) === 'number' ? response.group.assignment : response.group.assignment.id; + const cid = typeof response.group.class === "string" ? response.group.class : response.group.class.id; + const an = + typeof response.group.assignment === "number" + ? response.group.assignment + : response.group.assignment.id; const gn = response.group.groupNumber; await invalidateAllGroupKeys(queryClient, cid, an, gn); @@ -132,8 +161,8 @@ export function useUpdateGroupMutation(): UseMutationReturnType, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { @@ -147,8 +176,8 @@ export function useGroupSubmissionsQuery( } export function useGroupQuestionsQuery( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { diff --git a/frontend/src/queries/submissions.ts b/frontend/src/queries/submissions.ts index 5360afaf..97effd15 100644 --- a/frontend/src/queries/submissions.ts +++ b/frontend/src/queries/submissions.ts @@ -1,81 +1,96 @@ import { SubmissionController, type SubmissionResponse, type SubmissionsResponse } from "@/controllers/submissions"; import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission"; -import { QueryClient, useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; +import { + QueryClient, + useMutation, + useQuery, + useQueryClient, + type UseMutationReturnType, + type UseQueryReturnType, +} from "@tanstack/vue-query"; import { computed, toValue, type MaybeRefOrGetter } from "vue"; function submissionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number, full: boolean) { - return [ "submissions", classid, assignmentNumber, groupNumber, full ]; + return ["submissions", classid, assignmentNumber, groupNumber, full]; } function submissionQueryKey(classid: string, assignmentNumber: number, groupNumber: number, submissionNumber: number) { - return [ "submission", classid, assignmentNumber, groupNumber, submissionNumber ]; + return ["submission", classid, assignmentNumber, groupNumber, submissionNumber]; } export async function invalidateAllSubmissionKeys( - queryClient: QueryClient, - classid?: string, + queryClient: QueryClient, + classid?: string, assignmentNumber?: number, groupNumber?: number, submissionNumber?: number, ) { - const keys = [ - "submission", - ]; + const keys = ["submission"]; for (const key of keys) { - const queryKey = [key, classid, assignmentNumber, groupNumber, submissionNumber].filter(arg => arg !== undefined); + const queryKey = [key, classid, assignmentNumber, groupNumber, submissionNumber].filter( + (arg) => arg !== undefined, + ); await queryClient.invalidateQueries({ queryKey: queryKey }); } - await queryClient.invalidateQueries({ queryKey: [ "submissions", classid, assignmentNumber, groupNumber ].filter(arg => arg !== undefined) }); - await queryClient.invalidateQueries({ queryKey: [ "group-submissions", classid, assignmentNumber, groupNumber ].filter(arg => arg !== undefined) }); - await queryClient.invalidateQueries({ queryKey: [ "assignment-submissions", classid, assignmentNumber ].filter(arg => arg !== undefined) }); + await queryClient.invalidateQueries({ + queryKey: ["submissions", classid, assignmentNumber, groupNumber].filter((arg) => arg !== undefined), + }); + await queryClient.invalidateQueries({ + queryKey: ["group-submissions", classid, assignmentNumber, groupNumber].filter((arg) => arg !== undefined), + }); + await queryClient.invalidateQueries({ + queryKey: ["assignment-submissions", classid, assignmentNumber].filter((arg) => arg !== undefined), + }); } function checkEnabled( - classid: string | undefined, - assignmentNumber: number | undefined, + classid: string | undefined, + assignmentNumber: number | undefined, groupNumber: number | undefined, - submissionNumber: number | undefined + submissionNumber: number | undefined, ): boolean { - return Boolean(classid) - && !isNaN(Number(groupNumber)) - && !isNaN(Number(assignmentNumber)) - && !isNaN(Number(submissionNumber)); + return ( + Boolean(classid) && + !isNaN(Number(groupNumber)) && + !isNaN(Number(assignmentNumber)) && + !isNaN(Number(submissionNumber)) + ); } function toValues( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, submissionNumber: MaybeRefOrGetter, full: MaybeRefOrGetter, ) { - return { - cid: toValue(classid), - an: toValue(assignmentNumber), - gn: toValue(groupNumber), - sn: toValue(submissionNumber), - f: toValue(full) + return { + cid: toValue(classid), + an: toValue(assignmentNumber), + gn: toValue(groupNumber), + sn: toValue(submissionNumber), + f: toValue(full), }; } export function useSubmissionsQuery( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, - groupNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, + groupNumber: MaybeRefOrGetter, full: MaybeRefOrGetter = true, ): UseQueryReturnType { const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, 1, full); return useQuery({ - queryKey: computed(() => (submissionsQueryKey(cid!, an!, gn!, f))), + queryKey: computed(() => submissionsQueryKey(cid!, an!, gn!, f)), queryFn: async () => new SubmissionController(cid!, an!, gn!).getAll(f), enabled: () => checkEnabled(cid, an, gn, sn), }); } export function useSubmissionQuery( - classid: MaybeRefOrGetter, - assignmentNumber: MaybeRefOrGetter, + classid: MaybeRefOrGetter, + assignmentNumber: MaybeRefOrGetter, groupNumber: MaybeRefOrGetter, ): UseQueryReturnType { const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, 1, true); @@ -87,20 +102,25 @@ export function useSubmissionQuery( }); } -export function useCreateSubmissionMutation(): UseMutationReturnType { +export function useCreateSubmissionMutation(): UseMutationReturnType< + SubmissionResponse, + Error, + { cid: string; an: number; gn: number; data: SubmissionDTO }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({cid, an, gn, data}) => new SubmissionController(cid, an, gn).createSubmission(data), + mutationFn: async ({ cid, an, gn, data }) => new SubmissionController(cid, an, gn).createSubmission(data), onSuccess: async (response) => { if (!response.submission.group) { await invalidateAllSubmissionKeys(queryClient); } else { const cls = response.submission.group.class; const assignment = response.submission.group.assignment; - - const cid = typeof(cls) === 'string' ? cls : cls.id; - const an = typeof(assignment) === 'number' ? assignment : assignment.id; + + const cid = typeof cls === "string" ? cls : cls.id; + const an = typeof assignment === "number" ? assignment : assignment.id; const gn = response.submission.group.groupNumber; await invalidateAllSubmissionKeys(queryClient, cid, an, gn); @@ -109,24 +129,29 @@ export function useCreateSubmissionMutation(): UseMutationReturnType { +export function useDeleteSubmissionMutation(): UseMutationReturnType< + SubmissionResponse, + Error, + { cid: string; an: number; gn: number; sn: number }, + unknown +> { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({cid, an, gn, sn}) => new SubmissionController(cid, an, gn).deleteSubmission(sn), + mutationFn: async ({ cid, an, gn, sn }) => new SubmissionController(cid, an, gn).deleteSubmission(sn), onSuccess: async (response) => { if (!response.submission.group) { await invalidateAllSubmissionKeys(queryClient); } else { const cls = response.submission.group.class; const assignment = response.submission.group.assignment; - - const cid = typeof(cls) === 'string' ? cls : cls.id; - const an = typeof(assignment) === 'number' ? assignment : assignment.id; + + const cid = typeof cls === "string" ? cls : cls.id; + const an = typeof assignment === "number" ? assignment : assignment.id; const gn = response.submission.group.groupNumber; await invalidateAllSubmissionKeys(queryClient, cid, an, gn); } }, }); -} \ No newline at end of file +} From e995419227a9bad743ac90e80656ad3871a34abc Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Sun, 13 Apr 2025 15:01:22 +0200 Subject: [PATCH 54/71] fix: fixed linting error --- frontend/src/controllers/submissions.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/controllers/submissions.ts b/frontend/src/controllers/submissions.ts index 1f281e62..0d9c73f0 100644 --- a/frontend/src/controllers/submissions.ts +++ b/frontend/src/controllers/submissions.ts @@ -14,10 +14,6 @@ export class SubmissionController extends BaseController { super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}/submissions`); } - protected getBasePath(classid: string, assignmentNumber: number, groupNumber: number) { - return `class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}/submissions`; - } - async getAll(full = true): Promise { return this.get(`/`, { full }); } From 834e991568fbcdc50eb5f5f138d433d644ca6309 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sun, 13 Apr 2025 19:40:20 +0200 Subject: [PATCH 55/71] fix: invitation geeft enkel classId field terug --- backend/src/interfaces/teacher-invitation.ts | 4 ++-- backend/tests/setup-tests.ts | 8 ++++++-- common/src/interfaces/teacher-invitation.ts | 2 +- frontend/src/views/classes/TeacherClasses.vue | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index 98189938..bc2fd4af 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -10,7 +10,7 @@ export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): Teache return { sender: mapToUserDTO(invitation.sender), receiver: mapToUserDTO(invitation.receiver), - class: mapToClassDTO(invitation.class), + classId: invitation.class.classId!, }; } @@ -18,7 +18,7 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea return { sender: invitation.sender.username, receiver: invitation.receiver.username, - class: invitation.class.classId!, + classId: invitation.class.classId!, }; } diff --git a/backend/tests/setup-tests.ts b/backend/tests/setup-tests.ts index 5bd2fbd6..b666fbd2 100644 --- a/backend/tests/setup-tests.ts +++ b/backend/tests/setup-tests.ts @@ -13,6 +13,7 @@ import { makeTestAttachments } from './test_assets/content/attachments.testdata. import { makeTestQuestions } from './test_assets/questions/questions.testdata.js'; import { makeTestAnswers } from './test_assets/questions/answers.testdata.js'; import { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js'; +import {Collection} from "@mikro-orm/core"; export async function setupTestApp(): Promise { dotenv.config({ path: '.env.test' }); @@ -28,8 +29,8 @@ export async function setupTestApp(): Promise { const assignments = makeTestAssignemnts(em, classes); const groups = makeTestGroups(em, students, assignments); - assignments[0].groups = groups.slice(0, 3); - assignments[1].groups = groups.slice(3, 4); + assignments[0].groups = new Collection(groups.slice(0, 3)); + assignments[1].groups = new Collection(groups.slice(3, 4)); const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes); const classJoinRequests = makeTestClassJoinRequests(em, students, classes); @@ -41,6 +42,9 @@ export async function setupTestApp(): Promise { const answers = makeTestAnswers(em, teachers, questions); const submissions = makeTestSubmissions(em, students, groups); + console.log("classes", classes); + console.log("invitations", teacherInvitations); + await em.persistAndFlush([ ...students, ...teachers, diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index c61f9a6a..c34f46f7 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -4,7 +4,7 @@ import { ClassDTO } from './class'; export interface TeacherInvitationDTO { sender: string | UserDTO; receiver: string | UserDTO; - class: string | ClassDTO; + classId: string; } export interface TeacherInvitationData { diff --git a/frontend/src/views/classes/TeacherClasses.vue b/frontend/src/views/classes/TeacherClasses.vue index ae673d99..010cf2e2 100644 --- a/frontend/src/views/classes/TeacherClasses.vue +++ b/frontend/src/views/classes/TeacherClasses.vue @@ -276,10 +276,10 @@ - {{ (i.class as ClassDTO).displayName }} + {{ i.classId }} {{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }} From 17dc9649c5b4c4048586dd361cabc2049fd83118 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sun, 13 Apr 2025 20:50:15 +0200 Subject: [PATCH 56/71] feat: controller + queries --- frontend/src/controllers/classes.ts | 9 +-- .../src/controllers/teacher-invitations.ts | 28 ++++++++ frontend/src/queries/teacher-invitations.ts | 65 +++++++++++++++++++ 3 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 frontend/src/controllers/teacher-invitations.ts create mode 100644 frontend/src/queries/teacher-invitations.ts diff --git a/frontend/src/controllers/classes.ts b/frontend/src/controllers/classes.ts index 03e3f560..65a992a5 100644 --- a/frontend/src/controllers/classes.ts +++ b/frontend/src/controllers/classes.ts @@ -4,6 +4,7 @@ import type { StudentsResponse } from "./students"; import type { AssignmentsResponse } from "./assignments"; import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation"; import type { TeachersResponse } from "@/controllers/teachers.ts"; +import type {TeacherInvitationsResponse} from "@/controllers/teacher-invitations.ts"; export interface ClassesResponse { classes: ClassDTO[] | string[]; @@ -13,14 +14,6 @@ export interface ClassResponse { class: ClassDTO; } -export interface TeacherInvitationsResponse { - invites: TeacherInvitationDTO[]; -} - -export interface TeacherInvitationResponse { - invite: TeacherInvitationDTO; -} - export class ClassController extends BaseController { constructor() { super("class"); diff --git a/frontend/src/controllers/teacher-invitations.ts b/frontend/src/controllers/teacher-invitations.ts new file mode 100644 index 00000000..30201821 --- /dev/null +++ b/frontend/src/controllers/teacher-invitations.ts @@ -0,0 +1,28 @@ +import {BaseController} from "@/controllers/base-controller.ts"; +import type {TeacherInvitationData, TeacherInvitationDTO} from "@dwengo-1/common/interfaces/teacher-invitation"; + +export interface TeacherInvitationsResponse { + invitations: TeacherInvitationDTO[] +} + +export interface TeacherInvitationResponse { + invitation: TeacherInvitationDTO +} + +export class TeacherInvitationController extends BaseController { + constructor() { + super("teachers/invitations"); + } + + async getAll(username: string, by: boolean): Promise { + return this.get(`/${username}`, { by }); + } + + async create(data: TeacherInvitationData): Promise { + return this.post("/", data); + } + + async respond(data: TeacherInvitationData, accepted: boolean): Promise { + return this.delete(`/${data.sender}/${data.receiver}/${data.class}`, { accepted }); + } +} diff --git a/frontend/src/queries/teacher-invitations.ts b/frontend/src/queries/teacher-invitations.ts new file mode 100644 index 00000000..e0b1e957 --- /dev/null +++ b/frontend/src/queries/teacher-invitations.ts @@ -0,0 +1,65 @@ +import { + useMutation, + useQuery, + type UseMutationReturnType, + type UseQueryReturnType, +} from "@tanstack/vue-query"; +import { computed, toValue } from "vue"; +import type { MaybeRefOrGetter } from "vue"; +import { + TeacherInvitationController, + type TeacherInvitationResponse, + type TeacherInvitationsResponse +} from "@/controllers/teacher-invitations.ts"; +import type {TeacherInvitationData} from "@dwengo-1/common/dist/interfaces/teacher-invitation.ts"; +import type {TeacherDTO} from "@dwengo-1/common/dist/interfaces/teacher.ts"; + +const controller = new TeacherInvitationController(); + +/** + all the invitations the teacher send +**/ +export function useTeacherInvitationsByQuery(username: MaybeRefOrGetter +): UseQueryReturnType { + return useQuery({ + queryFn: computed(() => controller.getAll(toValue(username), true)), + enabled: () => Boolean(toValue(username)), + }) +} + +/** + all the pending invitations send to this teacher + */ +export function useTeacherInvitationsForQuery(username: MaybeRefOrGetter +): UseQueryReturnType { + return useQuery({ + queryFn: computed(() => controller.getAll(toValue(username), false)), + enabled: () => Boolean(toValue(username)), + }) +} + +export function useCreateTeacherInvitationMutation(): UseMutationReturnType{ + return useMutation({ + mutationFn: async (data: TeacherInvitationData) => controller.create(data) + }) +} + +export function useAcceptTeacherInvitationMutation(): UseMutationReturnType { + return useMutation({ + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, true) + }) +} + +export function useDeclineTeacherInvitationMutation(): UseMutationReturnType { + return useMutation({ + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false) + }) +} + +export function useDeleteTeacherInvitationMutation(): UseMutationReturnType { + return useMutation({ + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false) + }) +} + + From a824cdff9475d9fd4444d428080bdcd500f98f72 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sun, 13 Apr 2025 20:54:05 +0200 Subject: [PATCH 57/71] fix: lint --- backend/src/interfaces/teacher-invitation.ts | 1 - backend/src/services/teacher-invitations.ts | 2 +- backend/tests/setup-tests.ts | 3 --- common/src/interfaces/teacher-invitation.ts | 1 - frontend/src/controllers/classes.ts | 1 - frontend/src/queries/teacher-invitations.ts | 8 ++++---- 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index bc2fd4af..87741e3d 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -1,5 +1,4 @@ import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; -import { mapToClassDTO } from './class.js'; import { mapToUserDTO } from './user.js'; import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; import {getTeacherInvitationRepository} from "../data/repositories"; diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts index 1b9ef179..08886eb2 100644 --- a/backend/src/services/teacher-invitations.ts +++ b/backend/src/services/teacher-invitations.ts @@ -50,7 +50,7 @@ async function fetchInvitation(sender: Teacher, receiver: Teacher, cls: Class): return invite; } -export async function deleteInvitationFor(usernameSender: string, usernameReceiver: string, classId: string, accepted: boolean) { +export async function deleteInvitationFor(usernameSender: string, usernameReceiver: string, classId: string, accepted: boolean): Promise { const teacherInvitationRepository = getTeacherInvitationRepository(); const sender = await fetchTeacher(usernameSender); const receiver = await fetchTeacher(usernameReceiver); diff --git a/backend/tests/setup-tests.ts b/backend/tests/setup-tests.ts index b666fbd2..b0e5ef5b 100644 --- a/backend/tests/setup-tests.ts +++ b/backend/tests/setup-tests.ts @@ -42,9 +42,6 @@ export async function setupTestApp(): Promise { const answers = makeTestAnswers(em, teachers, questions); const submissions = makeTestSubmissions(em, students, groups); - console.log("classes", classes); - console.log("invitations", teacherInvitations); - await em.persistAndFlush([ ...students, ...teachers, diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index c34f46f7..b952b051 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -1,5 +1,4 @@ import { UserDTO } from './user'; -import { ClassDTO } from './class'; export interface TeacherInvitationDTO { sender: string | UserDTO; diff --git a/frontend/src/controllers/classes.ts b/frontend/src/controllers/classes.ts index 65a992a5..9020e166 100644 --- a/frontend/src/controllers/classes.ts +++ b/frontend/src/controllers/classes.ts @@ -2,7 +2,6 @@ import { BaseController } from "./base-controller"; import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; import type { StudentsResponse } from "./students"; import type { AssignmentsResponse } from "./assignments"; -import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation"; import type { TeachersResponse } from "@/controllers/teachers.ts"; import type {TeacherInvitationsResponse} from "@/controllers/teacher-invitations.ts"; diff --git a/frontend/src/queries/teacher-invitations.ts b/frontend/src/queries/teacher-invitations.ts index e0b1e957..4d9c5a52 100644 --- a/frontend/src/queries/teacher-invitations.ts +++ b/frontend/src/queries/teacher-invitations.ts @@ -17,23 +17,23 @@ import type {TeacherDTO} from "@dwengo-1/common/dist/interfaces/teacher.ts"; const controller = new TeacherInvitationController(); /** - all the invitations the teacher send + All the invitations the teacher send **/ export function useTeacherInvitationsByQuery(username: MaybeRefOrGetter ): UseQueryReturnType { return useQuery({ - queryFn: computed(() => controller.getAll(toValue(username), true)), + queryFn: computed(async () => controller.getAll(toValue(username), true)), enabled: () => Boolean(toValue(username)), }) } /** - all the pending invitations send to this teacher + All the pending invitations send to this teacher */ export function useTeacherInvitationsForQuery(username: MaybeRefOrGetter ): UseQueryReturnType { return useQuery({ - queryFn: computed(() => controller.getAll(toValue(username), false)), + queryFn: computed(async () => controller.getAll(toValue(username), false)), enabled: () => Boolean(toValue(username)), }) } From 783c91b2e37e6cf9ad21a23ba82343811ee552e1 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Sun, 13 Apr 2025 18:59:56 +0000 Subject: [PATCH 58/71] style: fix linting issues met Prettier --- .../src/controllers/teacher-invitations.ts | 6 +- .../classes/teacher-invitation-repository.ts | 2 +- backend/src/interfaces/teacher-invitation.ts | 10 +-- backend/src/routes/teacher-invitations.ts | 9 +-- backend/src/services/teacher-invitations.ts | 42 +++++------ .../controllers/teacher-invitations.test.ts | 52 +++++++------- backend/tests/setup-tests.ts | 2 +- frontend/src/controllers/classes.ts | 2 +- .../src/controllers/teacher-invitations.ts | 8 +-- frontend/src/queries/teacher-invitations.ts | 69 +++++++++++-------- frontend/src/views/classes/TeacherClasses.vue | 3 +- 11 files changed, 112 insertions(+), 93 deletions(-) diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index 15bfc936..b826dee3 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; -import {requireFields} from "./error-helper"; -import {createInvitation, deleteInvitationFor, getAllInvitations} from "../services/teacher-invitations"; -import {TeacherInvitationData} from "@dwengo-1/common/interfaces/teacher-invitation"; +import { requireFields } from './error-helper'; +import { createInvitation, deleteInvitationFor, getAllInvitations } from '../services/teacher-invitations'; +import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; export async function getAllInvitationsHandler(req: Request, res: Response): Promise { const username = req.params.username; diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index 5461d29b..69c0a972 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -25,6 +25,6 @@ export class TeacherInvitationRepository extends DwengoEntityRepository { const teacher = await fetchTeacher(username); @@ -29,12 +29,12 @@ export async function createInvitation(data: TeacherInvitationData): Promise { +export async function deleteInvitationFor( + usernameSender: string, + usernameReceiver: string, + classId: string, + accepted: boolean +): Promise { const teacherInvitationRepository = getTeacherInvitationRepository(); const sender = await fetchTeacher(usernameSender); const receiver = await fetchTeacher(usernameReceiver); @@ -60,12 +65,9 @@ export async function deleteInvitationFor(usernameSender: string, usernameReceiv const invitation = await fetchInvitation(sender, receiver, cls); await teacherInvitationRepository.deleteBy(cls, sender, receiver); - if (accepted){ + if (accepted) { await addClassTeacher(classId, usernameReceiver); } return mapToTeacherInvitationDTO(invitation); } - - - diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index 8135281a..6f233898 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -1,13 +1,9 @@ import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { Request, Response } from 'express'; import { setupTestApp } from '../setup-tests.js'; -import { - createInvitationHandler, - deleteInvitationForHandler, - getAllInvitationsHandler -} from "../../src/controllers/teacher-invitations"; -import {TeacherInvitationData} from "@dwengo-1/common/interfaces/teacher-invitation"; -import {getClassHandler} from "../../src/controllers/classes"; +import { createInvitationHandler, deleteInvitationForHandler, getAllInvitationsHandler } from '../../src/controllers/teacher-invitations'; +import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; +import { getClassHandler } from '../../src/controllers/classes'; describe('Teacher controllers', () => { let req: Partial; @@ -27,22 +23,22 @@ describe('Teacher controllers', () => { }); it('Get teacher invitations by', async () => { - req = {params: {username: 'LimpBizkit'}, query: {by: 'true' }}; + req = { params: { username: 'LimpBizkit' }, query: { by: 'true' } }; await getAllInvitationsHandler(req as Request, res as Response); - expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({invitations: expect.anything()})); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; expect(result.invitations).to.have.length.greaterThan(0); }); it('Get teacher invitations for', async () => { - req = {params: {username: 'FooFighters'}, query: {by: 'false' }}; + req = { params: { username: 'FooFighters' }, query: { by: 'false' } }; await getAllInvitationsHandler(req as Request, res as Response); - expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({invitations: expect.anything()})); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; expect(result.invitations).to.have.length.greaterThan(0); @@ -50,8 +46,9 @@ describe('Teacher controllers', () => { it('Create and delete invitation', async () => { const body = { - sender: 'LimpBizkit', receiver: 'testleerkracht1', - class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + sender: 'LimpBizkit', + receiver: 'testleerkracht1', + class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', } as TeacherInvitationData; req = { body }; @@ -59,9 +56,11 @@ describe('Teacher controllers', () => { req = { params: { - sender: 'LimpBizkit', receiver: 'testleerkracht1', - classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' - }, body: { accepted: 'false' } + sender: 'LimpBizkit', + receiver: 'testleerkracht1', + classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', + }, + body: { accepted: 'false' }, }; await deleteInvitationForHandler(req as Request, res as Response); @@ -69,8 +68,9 @@ describe('Teacher controllers', () => { it('Create and accept invitation', async () => { const body = { - sender: 'LimpBizkit', receiver: 'testleerkracht1', - class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + sender: 'LimpBizkit', + receiver: 'testleerkracht1', + class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', } as TeacherInvitationData; req = { body }; @@ -78,16 +78,20 @@ describe('Teacher controllers', () => { req = { params: { - sender: 'LimpBizkit', receiver: 'testleerkracht1', - classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' - }, body: { accepted: 'true' } + sender: 'LimpBizkit', + receiver: 'testleerkracht1', + classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', + }, + body: { accepted: 'true' }, }; await deleteInvitationForHandler(req as Request, res as Response); - req = {params: { - id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' - }}; + req = { + params: { + id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', + }, + }; await getClassHandler(req as Request, res as Response); diff --git a/backend/tests/setup-tests.ts b/backend/tests/setup-tests.ts index b0e5ef5b..699e081b 100644 --- a/backend/tests/setup-tests.ts +++ b/backend/tests/setup-tests.ts @@ -13,7 +13,7 @@ import { makeTestAttachments } from './test_assets/content/attachments.testdata. import { makeTestQuestions } from './test_assets/questions/questions.testdata.js'; import { makeTestAnswers } from './test_assets/questions/answers.testdata.js'; import { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js'; -import {Collection} from "@mikro-orm/core"; +import { Collection } from '@mikro-orm/core'; export async function setupTestApp(): Promise { dotenv.config({ path: '.env.test' }); diff --git a/frontend/src/controllers/classes.ts b/frontend/src/controllers/classes.ts index 9020e166..c9b7f6fa 100644 --- a/frontend/src/controllers/classes.ts +++ b/frontend/src/controllers/classes.ts @@ -3,7 +3,7 @@ import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; import type { StudentsResponse } from "./students"; import type { AssignmentsResponse } from "./assignments"; import type { TeachersResponse } from "@/controllers/teachers.ts"; -import type {TeacherInvitationsResponse} from "@/controllers/teacher-invitations.ts"; +import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations.ts"; export interface ClassesResponse { classes: ClassDTO[] | string[]; diff --git a/frontend/src/controllers/teacher-invitations.ts b/frontend/src/controllers/teacher-invitations.ts index 30201821..a6e2045a 100644 --- a/frontend/src/controllers/teacher-invitations.ts +++ b/frontend/src/controllers/teacher-invitations.ts @@ -1,12 +1,12 @@ -import {BaseController} from "@/controllers/base-controller.ts"; -import type {TeacherInvitationData, TeacherInvitationDTO} from "@dwengo-1/common/interfaces/teacher-invitation"; +import { BaseController } from "@/controllers/base-controller.ts"; +import type { TeacherInvitationData, TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation"; export interface TeacherInvitationsResponse { - invitations: TeacherInvitationDTO[] + invitations: TeacherInvitationDTO[]; } export interface TeacherInvitationResponse { - invitation: TeacherInvitationDTO + invitation: TeacherInvitationDTO; } export class TeacherInvitationController extends BaseController { diff --git a/frontend/src/queries/teacher-invitations.ts b/frontend/src/queries/teacher-invitations.ts index 4d9c5a52..a56ea9cd 100644 --- a/frontend/src/queries/teacher-invitations.ts +++ b/frontend/src/queries/teacher-invitations.ts @@ -1,65 +1,80 @@ -import { - useMutation, - useQuery, - type UseMutationReturnType, - type UseQueryReturnType, -} from "@tanstack/vue-query"; +import { useMutation, useQuery, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue } from "vue"; import type { MaybeRefOrGetter } from "vue"; import { TeacherInvitationController, type TeacherInvitationResponse, - type TeacherInvitationsResponse + type TeacherInvitationsResponse, } from "@/controllers/teacher-invitations.ts"; -import type {TeacherInvitationData} from "@dwengo-1/common/dist/interfaces/teacher-invitation.ts"; -import type {TeacherDTO} from "@dwengo-1/common/dist/interfaces/teacher.ts"; +import type { TeacherInvitationData } from "@dwengo-1/common/dist/interfaces/teacher-invitation.ts"; +import type { TeacherDTO } from "@dwengo-1/common/dist/interfaces/teacher.ts"; const controller = new TeacherInvitationController(); /** All the invitations the teacher send **/ -export function useTeacherInvitationsByQuery(username: MaybeRefOrGetter +export function useTeacherInvitationsByQuery( + username: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ queryFn: computed(async () => controller.getAll(toValue(username), true)), enabled: () => Boolean(toValue(username)), - }) + }); } /** All the pending invitations send to this teacher */ -export function useTeacherInvitationsForQuery(username: MaybeRefOrGetter +export function useTeacherInvitationsForQuery( + username: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ queryFn: computed(async () => controller.getAll(toValue(username), false)), enabled: () => Boolean(toValue(username)), - }) + }); } -export function useCreateTeacherInvitationMutation(): UseMutationReturnType{ +export function useCreateTeacherInvitationMutation(): UseMutationReturnType< + TeacherInvitationResponse, + Error, + TeacherDTO, + unknown +> { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.create(data) - }) + mutationFn: async (data: TeacherInvitationData) => controller.create(data), + }); } -export function useAcceptTeacherInvitationMutation(): UseMutationReturnType { +export function useAcceptTeacherInvitationMutation(): UseMutationReturnType< + TeacherInvitationResponse, + Error, + TeacherDTO, + unknown +> { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, true) - }) + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, true), + }); } -export function useDeclineTeacherInvitationMutation(): UseMutationReturnType { +export function useDeclineTeacherInvitationMutation(): UseMutationReturnType< + TeacherInvitationResponse, + Error, + TeacherDTO, + unknown +> { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false) - }) + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false), + }); } -export function useDeleteTeacherInvitationMutation(): UseMutationReturnType { +export function useDeleteTeacherInvitationMutation(): UseMutationReturnType< + TeacherInvitationResponse, + Error, + TeacherDTO, + unknown +> { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false) - }) + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false), + }); } - - diff --git a/frontend/src/views/classes/TeacherClasses.vue b/frontend/src/views/classes/TeacherClasses.vue index 010cf2e2..a3fba26e 100644 --- a/frontend/src/views/classes/TeacherClasses.vue +++ b/frontend/src/views/classes/TeacherClasses.vue @@ -279,7 +279,8 @@ :key="i.classId" > - {{ i.classId }} + {{ i.classId }} + {{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }} From f3d2b3313c43f30e75b52f020600bf9d2e6fe2b3 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Mon, 14 Apr 2025 16:50:54 +0200 Subject: [PATCH 59/71] feat: status teacher invite --- .../src/controllers/teacher-invitations.ts | 54 +++++++++++--- .../classes/class-join-request-repository.ts | 4 +- .../classes/teacher-invitation-repository.ts | 11 +-- .../classes/class-join-request.entity.ts | 6 +- .../classes/teacher-invitation.entity.ts | 6 +- backend/src/interfaces/student-request.ts | 4 +- backend/src/interfaces/teacher-invitation.ts | 16 +++-- backend/src/routes/teacher-invitations.ts | 13 +++- backend/src/services/teacher-invitations.ts | 70 +++++++++++-------- backend/src/services/teachers.ts | 6 +- .../controllers/teacher-invitations.test.ts | 41 +++++++++-- .../classes/class-join-requests.testdata.ts | 10 +-- .../classes/teacher-invitations.testdata.ts | 13 ++-- common/src/interfaces/class-join-request.ts | 4 +- common/src/interfaces/teacher-invitation.ts | 3 + common/src/util/class-join-request.ts | 2 +- 16 files changed, 184 insertions(+), 79 deletions(-) diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index b826dee3..f7cfa05a 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -1,11 +1,17 @@ -import { Request, Response } from 'express'; -import { requireFields } from './error-helper'; -import { createInvitation, deleteInvitationFor, getAllInvitations } from '../services/teacher-invitations'; -import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; +import {Request, Response} from 'express'; +import {requireFields} from './error-helper'; +import { + createInvitation, + deleteInvitation, + getAllInvitations, + getInvitation, + updateInvitation +} from '../services/teacher-invitations'; +import {TeacherInvitationData} from '@dwengo-1/common/interfaces/teacher-invitation'; export async function getAllInvitationsHandler(req: Request, res: Response): Promise { const username = req.params.username; - const by = req.query.by === 'true'; + const by = req.query.sent === 'true'; requireFields({ username }); const invitations = await getAllInvitations(username, by); @@ -13,6 +19,17 @@ export async function getAllInvitationsHandler(req: Request, res: Response): Pro res.json({ invitations }); } +export async function getInvitationHandler(req: Request, res: Response): Promise { + const sender = req.params.sender; + const receiver = req.params.receiver; + const classId = req.params.classId; + requireFields({ sender, receiver, classId }); + + const invitation = await getInvitation(sender, receiver, classId); + + res.json({ invitation }); +} + export async function createInvitationHandler(req: Request, res: Response): Promise { const sender = req.body.sender; const receiver = req.body.receiver; @@ -25,14 +42,29 @@ export async function createInvitationHandler(req: Request, res: Response): Prom res.json({ invitation }); } -export async function deleteInvitationForHandler(req: Request, res: Response): Promise { - const sender = req.params.sender; - const receiver = req.params.receiver; - const classId = req.params.classId; - const accepted = req.body.accepted !== 'false'; +export async function updateInvitationHandler(req: Request, res: Response): Promise { + const sender = req.body.sender; + const receiver = req.body.receiver; + const classId = req.body.class; + req.body.accepted = req.body.accepted !== 'false'; requireFields({ sender, receiver, classId }); - const invitation = await deleteInvitationFor(sender, receiver, classId, accepted); + const data = req.body as TeacherInvitationData; + const invitation = await updateInvitation(data); + + res.json({ invitation }); +} + +export async function deleteInvitationHandler(req: Request, res: Response): Promise { + const sender = req.params.sender; + const receiver = req.params.receiver; + const classId = req.params.classId; + requireFields({ sender, receiver, classId }); + + const data: TeacherInvitationData = { + sender, receiver, class: classId + }; + const invitation = await deleteInvitation(data); res.json({ invitation }); } diff --git a/backend/src/data/classes/class-join-request-repository.ts b/backend/src/data/classes/class-join-request-repository.ts index 0d9ab6e1..8bd0f81e 100644 --- a/backend/src/data/classes/class-join-request-repository.ts +++ b/backend/src/data/classes/class-join-request-repository.ts @@ -2,14 +2,14 @@ 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'; +import { ClassStatus } 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, status: ClassJoinRequestStatus.Open } }); // TODO check if works like this + return this.findAll({ where: { class: clazz, status: ClassStatus.Open } }); // TODO check if works like this } public async findByStudentAndClass(requester: Student, clazz: Class): Promise { return this.findOne({ requester, class: clazz }); diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index 69c0a972..46abc685 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -1,7 +1,8 @@ -import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; -import { Class } from '../../entities/classes/class.entity.js'; -import { TeacherInvitation } from '../../entities/classes/teacher-invitation.entity.js'; -import { Teacher } from '../../entities/users/teacher.entity.js'; +import {DwengoEntityRepository} from '../dwengo-entity-repository.js'; +import {Class} from '../../entities/classes/class.entity.js'; +import {TeacherInvitation} from '../../entities/classes/teacher-invitation.entity.js'; +import {Teacher} from '../../entities/users/teacher.entity.js'; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; export class TeacherInvitationRepository extends DwengoEntityRepository { public async findAllInvitationsForClass(clazz: Class): Promise { @@ -11,7 +12,7 @@ export class TeacherInvitationRepository extends DwengoEntityRepository { - return this.findAll({ where: { receiver: receiver } }); + return this.findAll({ where: { receiver: receiver, status: ClassStatus.Open } }); } public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise { return this.deleteWhere({ diff --git a/backend/src/entities/classes/class-join-request.entity.ts b/backend/src/entities/classes/class-join-request.entity.ts index 907c0199..548968a6 100644 --- a/backend/src/entities/classes/class-join-request.entity.ts +++ b/backend/src/entities/classes/class-join-request.entity.ts @@ -2,7 +2,7 @@ import { Entity, Enum, ManyToOne } from '@mikro-orm/core'; import { Student } from '../users/student.entity.js'; import { Class } from './class.entity.js'; import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; -import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; @Entity({ repository: () => ClassJoinRequestRepository, @@ -20,6 +20,6 @@ export class ClassJoinRequest { }) class!: Class; - @Enum(() => ClassJoinRequestStatus) - status!: ClassJoinRequestStatus; + @Enum(() => ClassStatus) + status!: ClassStatus; } diff --git a/backend/src/entities/classes/teacher-invitation.entity.ts b/backend/src/entities/classes/teacher-invitation.entity.ts index 668a0a1c..b7451682 100644 --- a/backend/src/entities/classes/teacher-invitation.entity.ts +++ b/backend/src/entities/classes/teacher-invitation.entity.ts @@ -1,7 +1,8 @@ -import { Entity, ManyToOne } from '@mikro-orm/core'; +import {Entity, Enum, ManyToOne} from '@mikro-orm/core'; import { Teacher } from '../users/teacher.entity.js'; import { Class } from './class.entity.js'; import { TeacherInvitationRepository } from '../../data/classes/teacher-invitation-repository.js'; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; /** * Invitation of a teacher into a class (in order to teach it). @@ -25,4 +26,7 @@ export class TeacherInvitation { primary: true, }) class!: Class; + + @Enum(() => ClassStatus) + status!: ClassStatus; } diff --git a/backend/src/interfaces/student-request.ts b/backend/src/interfaces/student-request.ts index d97f5eb5..a4d3b31b 100644 --- a/backend/src/interfaces/student-request.ts +++ b/backend/src/interfaces/student-request.ts @@ -4,7 +4,7 @@ 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'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export function mapToStudentRequestDTO(request: ClassJoinRequest): ClassJoinRequestDTO { return { @@ -18,6 +18,6 @@ export function mapToStudentRequest(student: Student, cls: Class): ClassJoinRequ return getClassJoinRequestRepository().create({ requester: student, class: cls, - status: ClassJoinRequestStatus.Open, + status: ClassStatus.Open, }); } diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index 4d1284db..f6cd1698 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -1,15 +1,17 @@ -import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; -import { mapToUserDTO } from './user.js'; -import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; -import { getTeacherInvitationRepository } from '../data/repositories'; -import { Teacher } from '../entities/users/teacher.entity'; -import { Class } from '../entities/classes/class.entity'; +import {TeacherInvitation} from '../entities/classes/teacher-invitation.entity.js'; +import {mapToUserDTO} from './user.js'; +import {TeacherInvitationDTO} from '@dwengo-1/common/interfaces/teacher-invitation'; +import {getTeacherInvitationRepository} from '../data/repositories'; +import {Teacher} from '../entities/users/teacher.entity'; +import {Class} from '../entities/classes/class.entity'; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { return { sender: mapToUserDTO(invitation.sender), receiver: mapToUserDTO(invitation.receiver), classId: invitation.class.classId!, + status: invitation.status }; } @@ -18,6 +20,7 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea sender: invitation.sender.username, receiver: invitation.receiver.username, classId: invitation.class.classId!, + status: invitation.status }; } @@ -26,5 +29,6 @@ export function mapToInvitation(sender: Teacher, receiver: Teacher, cls: Class): sender, receiver, class: cls, + status: ClassStatus.Open, }); } diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts index 7af71fdc..6826ec21 100644 --- a/backend/src/routes/teacher-invitations.ts +++ b/backend/src/routes/teacher-invitations.ts @@ -1,12 +1,21 @@ import express from 'express'; -import { createInvitationHandler, deleteInvitationForHandler, getAllInvitationsHandler } from '../controllers/teacher-invitations'; +import { + createInvitationHandler, + deleteInvitationHandler, + getAllInvitationsHandler, getInvitationHandler, + updateInvitationHandler +} from '../controllers/teacher-invitations'; const router = express.Router({ mergeParams: true }); router.get('/:username', getAllInvitationsHandler); +router.get('/:sender/:receiver/:classId', getInvitationHandler); + router.post('/', createInvitationHandler); -router.delete('/:sender/:receiver/:classId', deleteInvitationForHandler); +router.put('/', updateInvitationHandler); + +router.delete('/:sender/:receiver/:classId', deleteInvitationHandler); export default router; diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts index dc54d5e5..19ead9a7 100644 --- a/backend/src/services/teacher-invitations.ts +++ b/backend/src/services/teacher-invitations.ts @@ -1,20 +1,19 @@ -import { fetchTeacher } from './teachers'; -import { getTeacherInvitationRepository } from '../data/repositories'; -import { mapToInvitation, mapToTeacherInvitationDTO } from '../interfaces/teacher-invitation'; -import { addClassTeacher, fetchClass } from './classes'; -import { TeacherInvitationData, TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; -import { ConflictException } from '../exceptions/conflict-exception'; -import { Teacher } from '../entities/users/teacher.entity'; -import { Class } from '../entities/classes/class.entity'; -import { NotFoundException } from '../exceptions/not-found-exception'; -import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity'; +import {fetchTeacher} from './teachers'; +import {getTeacherInvitationRepository} from '../data/repositories'; +import {mapToInvitation, mapToTeacherInvitationDTO} from '../interfaces/teacher-invitation'; +import {addClassTeacher, fetchClass} from './classes'; +import {TeacherInvitationData, TeacherInvitationDTO} from '@dwengo-1/common/interfaces/teacher-invitation'; +import {ConflictException} from '../exceptions/conflict-exception'; +import {NotFoundException} from '../exceptions/not-found-exception'; +import {TeacherInvitation} from '../entities/classes/teacher-invitation.entity'; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; -export async function getAllInvitations(username: string, by: boolean): Promise { +export async function getAllInvitations(username: string, sent: boolean): Promise { const teacher = await fetchTeacher(username); const teacherInvitationRepository = getTeacherInvitationRepository(); let invitations; - if (by) { + if (sent) { invitations = await teacherInvitationRepository.findAllInvitationsBy(teacher); } else { invitations = await teacherInvitationRepository.findAllInvitationsFor(teacher); @@ -39,7 +38,11 @@ export async function createInvitation(data: TeacherInvitationData): Promise { +async function fetchInvitation(usernameSender: string, usernameReceiver: string, classId: string): Promise { + const sender = await fetchTeacher(usernameSender); + const receiver = await fetchTeacher(usernameReceiver); + const cls = await fetchClass(classId); + const teacherInvitationRepository = getTeacherInvitationRepository(); const invite = await teacherInvitationRepository.findBy(cls, sender, receiver); @@ -50,24 +53,35 @@ async function fetchInvitation(sender: Teacher, receiver: Teacher, cls: Class): return invite; } -export async function deleteInvitationFor( - usernameSender: string, - usernameReceiver: string, - classId: string, - accepted: boolean -): Promise { - const teacherInvitationRepository = getTeacherInvitationRepository(); - const sender = await fetchTeacher(usernameSender); - const receiver = await fetchTeacher(usernameReceiver); +export async function getInvitation(sender: string, receiver: string, classId: string): Promise { + const invitation = await fetchInvitation(sender, receiver, classId); + return mapToTeacherInvitationDTO(invitation); +} - const cls = await fetchClass(classId); +export async function updateInvitation(data: TeacherInvitationData): Promise { + const invitation = await fetchInvitation(data.sender, data.receiver, data.class); + invitation.status = ClassStatus.Declined; - const invitation = await fetchInvitation(sender, receiver, cls); - await teacherInvitationRepository.deleteBy(cls, sender, receiver); - - if (accepted) { - await addClassTeacher(classId, usernameReceiver); + if (data.accepted) { + invitation.status = ClassStatus.Accepted; + await addClassTeacher(data.class, data.receiver); } + const teacherInvitationRepository = getTeacherInvitationRepository(); + await teacherInvitationRepository.save(invitation); + + return mapToTeacherInvitationDTO(invitation); +} + +export async function deleteInvitation(data: TeacherInvitationData): Promise { + const invitation = await fetchInvitation(data.sender, data.receiver, data.class); + + const sender = await fetchTeacher(data.sender); + const receiver = await fetchTeacher(data.receiver); + const cls = await fetchClass(data.class); + + const teacherInvitationRepository = getTeacherInvitationRepository(); + await teacherInvitationRepository.deleteBy(cls, sender, receiver); + return mapToTeacherInvitationDTO(invitation); } diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index e6596f9e..982b657b 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -28,7 +28,7 @@ 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 { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; -import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; import { ConflictException } from '../exceptions/conflict-exception.js'; export async function getAllTeachers(full: boolean): Promise { @@ -160,10 +160,10 @@ export async function updateClassJoinRequestStatus(studentUsername: string, clas throw new NotFoundException('Join request not found'); } - request.status = ClassJoinRequestStatus.Declined; + request.status = ClassStatus.Declined; if (accepted) { - request.status = ClassJoinRequestStatus.Accepted; + request.status = ClassStatus.Accepted; await addClassStudent(classId, studentUsername); } diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index 6f233898..005dac26 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -1,9 +1,15 @@ import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { Request, Response } from 'express'; import { setupTestApp } from '../setup-tests.js'; -import { createInvitationHandler, deleteInvitationForHandler, getAllInvitationsHandler } from '../../src/controllers/teacher-invitations'; +import { + createInvitationHandler, + deleteInvitationHandler, + getAllInvitationsHandler, + getInvitationHandler +} from '../../src/controllers/teacher-invitations'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; import { getClassHandler } from '../../src/controllers/classes'; +import {BadRequestException} from "../../src/exceptions/bad-request-exception"; describe('Teacher controllers', () => { let req: Partial; @@ -23,13 +29,14 @@ describe('Teacher controllers', () => { }); it('Get teacher invitations by', async () => { - req = { params: { username: 'LimpBizkit' }, query: { by: 'true' } }; + req = { params: { username: 'LimpBizkit' }, query: { sent: 'true' } }; await getAllInvitationsHandler(req as Request, res as Response); expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; + console.log(result.invitations); expect(result.invitations).to.have.length.greaterThan(0); }); @@ -63,9 +70,33 @@ describe('Teacher controllers', () => { body: { accepted: 'false' }, }; - await deleteInvitationForHandler(req as Request, res as Response); + await deleteInvitationHandler(req as Request, res as Response); }); + it('Get invitation', async () => { + req = { + params: { + sender: 'LimpBizkit', + receiver: 'FooFighters', + classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', + }, + }; + await getInvitationHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitation: expect.anything() })); + }); + + it('Get invitation error', async () => { + req = { + params: { no: 'no params' }, + }; + + await expect( async () => getInvitationHandler(req as Request, res as Response)) + .rejects.toThrowError(BadRequestException); + }); + + /* + it('Create and accept invitation', async () => { const body = { sender: 'LimpBizkit', @@ -85,7 +116,7 @@ describe('Teacher controllers', () => { body: { accepted: 'true' }, }; - await deleteInvitationForHandler(req as Request, res as Response); + await deleteInvitationHandler(req as Request, res as Response); req = { params: { @@ -98,4 +129,6 @@ describe('Teacher controllers', () => { const result = jsonMock.mock.lastCall?.[0]; expect(result.class.teachers).toContain('testleerkracht1'); }); + + */ }); diff --git a/backend/tests/test_assets/classes/class-join-requests.testdata.ts b/backend/tests/test_assets/classes/class-join-requests.testdata.ts index 32337b19..63319dc4 100644 --- a/backend/tests/test_assets/classes/class-join-requests.testdata.ts +++ b/backend/tests/test_assets/classes/class-join-requests.testdata.ts @@ -2,31 +2,31 @@ import { EntityManager } from '@mikro-orm/core'; import { ClassJoinRequest } from '../../../src/entities/classes/class-join-request.entity'; import { Student } from '../../../src/entities/users/student.entity'; import { Class } from '../../../src/entities/classes/class.entity'; -import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export function makeTestClassJoinRequests(em: EntityManager, students: Student[], classes: Class[]): ClassJoinRequest[] { const classJoinRequest01 = em.create(ClassJoinRequest, { requester: students[4], class: classes[1], - status: ClassJoinRequestStatus.Open, + status: ClassStatus.Open, }); const classJoinRequest02 = em.create(ClassJoinRequest, { requester: students[2], class: classes[1], - status: ClassJoinRequestStatus.Open, + status: ClassStatus.Open, }); const classJoinRequest03 = em.create(ClassJoinRequest, { requester: students[4], class: classes[2], - status: ClassJoinRequestStatus.Open, + status: ClassStatus.Open, }); const classJoinRequest04 = em.create(ClassJoinRequest, { requester: students[3], class: classes[2], - status: ClassJoinRequestStatus.Open, + status: ClassStatus.Open, }); return [classJoinRequest01, classJoinRequest02, classJoinRequest03, classJoinRequest04]; diff --git a/backend/tests/test_assets/classes/teacher-invitations.testdata.ts b/backend/tests/test_assets/classes/teacher-invitations.testdata.ts index 68204a57..35a3d1b8 100644 --- a/backend/tests/test_assets/classes/teacher-invitations.testdata.ts +++ b/backend/tests/test_assets/classes/teacher-invitations.testdata.ts @@ -1,31 +1,36 @@ -import { EntityManager } from '@mikro-orm/core'; -import { TeacherInvitation } from '../../../src/entities/classes/teacher-invitation.entity'; -import { Teacher } from '../../../src/entities/users/teacher.entity'; -import { Class } from '../../../src/entities/classes/class.entity'; +import {EntityManager} from '@mikro-orm/core'; +import {TeacherInvitation} from '../../../src/entities/classes/teacher-invitation.entity'; +import {Teacher} from '../../../src/entities/users/teacher.entity'; +import {Class} from '../../../src/entities/classes/class.entity'; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; export function makeTestTeacherInvitations(em: EntityManager, teachers: Teacher[], classes: Class[]): TeacherInvitation[] { const teacherInvitation01 = em.create(TeacherInvitation, { sender: teachers[1], receiver: teachers[0], class: classes[1], + status: ClassStatus.Open }); const teacherInvitation02 = em.create(TeacherInvitation, { sender: teachers[1], receiver: teachers[2], class: classes[1], + status: ClassStatus.Open }); const teacherInvitation03 = em.create(TeacherInvitation, { sender: teachers[2], receiver: teachers[0], class: classes[2], + status: ClassStatus.Open }); const teacherInvitation04 = em.create(TeacherInvitation, { sender: teachers[0], receiver: teachers[1], class: classes[0], + status: ClassStatus.Open }); return [teacherInvitation01, teacherInvitation02, teacherInvitation03, teacherInvitation04]; diff --git a/common/src/interfaces/class-join-request.ts b/common/src/interfaces/class-join-request.ts index 6787998b..5e8b2683 100644 --- a/common/src/interfaces/class-join-request.ts +++ b/common/src/interfaces/class-join-request.ts @@ -1,8 +1,8 @@ import { StudentDTO } from './student'; -import { ClassJoinRequestStatus } from '../util/class-join-request'; +import { ClassStatus } from '../util/class-join-request'; export interface ClassJoinRequestDTO { requester: StudentDTO; class: string; - status: ClassJoinRequestStatus; + status: ClassStatus; } diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index b952b051..37a4029f 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -1,13 +1,16 @@ import { UserDTO } from './user'; +import {ClassStatus} from "../util/class-join-request"; export interface TeacherInvitationDTO { sender: string | UserDTO; receiver: string | UserDTO; classId: string; + status: ClassStatus; } export interface TeacherInvitationData { sender: string; receiver: string; class: string; + accepted?: boolean; } diff --git a/common/src/util/class-join-request.ts b/common/src/util/class-join-request.ts index 5f9410f0..2049a16d 100644 --- a/common/src/util/class-join-request.ts +++ b/common/src/util/class-join-request.ts @@ -1,4 +1,4 @@ -export enum ClassJoinRequestStatus { +export enum ClassStatus { Open = 'open', Accepted = 'accepted', Declined = 'declined', From 8e2643f596033375339986b2021bc1c6fcfe5473 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Mon, 14 Apr 2025 14:52:59 +0000 Subject: [PATCH 60/71] style: fix linting issues met ESLint --- backend/tests/controllers/teacher-invitations.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index 005dac26..c7f61773 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -97,7 +97,7 @@ describe('Teacher controllers', () => { /* - it('Create and accept invitation', async () => { + It('Create and accept invitation', async () => { const body = { sender: 'LimpBizkit', receiver: 'testleerkracht1', From d88add83511c6d5a5ed9f06d9bd957a9dd4f20b4 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Mon, 14 Apr 2025 14:53:04 +0000 Subject: [PATCH 61/71] style: fix linting issues met Prettier --- backend/src/controllers/teacher-invitations.ts | 18 +++++++----------- .../classes/teacher-invitation-repository.ts | 10 +++++----- .../classes/teacher-invitation.entity.ts | 4 ++-- backend/src/interfaces/teacher-invitation.ts | 18 +++++++++--------- backend/src/routes/teacher-invitations.ts | 5 +++-- backend/src/services/teacher-invitations.ts | 18 +++++++++--------- .../controllers/teacher-invitations.test.ts | 7 +++---- .../classes/teacher-invitations.testdata.ts | 18 +++++++++--------- common/src/interfaces/teacher-invitation.ts | 2 +- 9 files changed, 48 insertions(+), 52 deletions(-) diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index f7cfa05a..4956f3e2 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -1,13 +1,7 @@ -import {Request, Response} from 'express'; -import {requireFields} from './error-helper'; -import { - createInvitation, - deleteInvitation, - getAllInvitations, - getInvitation, - updateInvitation -} from '../services/teacher-invitations'; -import {TeacherInvitationData} from '@dwengo-1/common/interfaces/teacher-invitation'; +import { Request, Response } from 'express'; +import { requireFields } from './error-helper'; +import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations'; +import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; export async function getAllInvitationsHandler(req: Request, res: Response): Promise { const username = req.params.username; @@ -62,7 +56,9 @@ export async function deleteInvitationHandler(req: Request, res: Response): Prom requireFields({ sender, receiver, classId }); const data: TeacherInvitationData = { - sender, receiver, class: classId + sender, + receiver, + class: classId, }; const invitation = await deleteInvitation(data); diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index 46abc685..c9442e29 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -1,8 +1,8 @@ -import {DwengoEntityRepository} from '../dwengo-entity-repository.js'; -import {Class} from '../../entities/classes/class.entity.js'; -import {TeacherInvitation} from '../../entities/classes/teacher-invitation.entity.js'; -import {Teacher} from '../../entities/users/teacher.entity.js'; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; +import { Class } from '../../entities/classes/class.entity.js'; +import { TeacherInvitation } from '../../entities/classes/teacher-invitation.entity.js'; +import { Teacher } from '../../entities/users/teacher.entity.js'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export class TeacherInvitationRepository extends DwengoEntityRepository { public async findAllInvitationsForClass(clazz: Class): Promise { diff --git a/backend/src/entities/classes/teacher-invitation.entity.ts b/backend/src/entities/classes/teacher-invitation.entity.ts index b7451682..6059f155 100644 --- a/backend/src/entities/classes/teacher-invitation.entity.ts +++ b/backend/src/entities/classes/teacher-invitation.entity.ts @@ -1,8 +1,8 @@ -import {Entity, Enum, ManyToOne} from '@mikro-orm/core'; +import { Entity, Enum, ManyToOne } from '@mikro-orm/core'; import { Teacher } from '../users/teacher.entity.js'; import { Class } from './class.entity.js'; import { TeacherInvitationRepository } from '../../data/classes/teacher-invitation-repository.js'; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; /** * Invitation of a teacher into a class (in order to teach it). diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index f6cd1698..8fef17af 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -1,17 +1,17 @@ -import {TeacherInvitation} from '../entities/classes/teacher-invitation.entity.js'; -import {mapToUserDTO} from './user.js'; -import {TeacherInvitationDTO} from '@dwengo-1/common/interfaces/teacher-invitation'; -import {getTeacherInvitationRepository} from '../data/repositories'; -import {Teacher} from '../entities/users/teacher.entity'; -import {Class} from '../entities/classes/class.entity'; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; +import { mapToUserDTO } from './user.js'; +import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; +import { getTeacherInvitationRepository } from '../data/repositories'; +import { Teacher } from '../entities/users/teacher.entity'; +import { Class } from '../entities/classes/class.entity'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { return { sender: mapToUserDTO(invitation.sender), receiver: mapToUserDTO(invitation.receiver), classId: invitation.class.classId!, - status: invitation.status + status: invitation.status, }; } @@ -20,7 +20,7 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea sender: invitation.sender.username, receiver: invitation.receiver.username, classId: invitation.class.classId!, - status: invitation.status + status: invitation.status, }; } diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts index 6826ec21..772b1351 100644 --- a/backend/src/routes/teacher-invitations.ts +++ b/backend/src/routes/teacher-invitations.ts @@ -2,8 +2,9 @@ import express from 'express'; import { createInvitationHandler, deleteInvitationHandler, - getAllInvitationsHandler, getInvitationHandler, - updateInvitationHandler + getAllInvitationsHandler, + getInvitationHandler, + updateInvitationHandler, } from '../controllers/teacher-invitations'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts index 19ead9a7..07f61bae 100644 --- a/backend/src/services/teacher-invitations.ts +++ b/backend/src/services/teacher-invitations.ts @@ -1,12 +1,12 @@ -import {fetchTeacher} from './teachers'; -import {getTeacherInvitationRepository} from '../data/repositories'; -import {mapToInvitation, mapToTeacherInvitationDTO} from '../interfaces/teacher-invitation'; -import {addClassTeacher, fetchClass} from './classes'; -import {TeacherInvitationData, TeacherInvitationDTO} from '@dwengo-1/common/interfaces/teacher-invitation'; -import {ConflictException} from '../exceptions/conflict-exception'; -import {NotFoundException} from '../exceptions/not-found-exception'; -import {TeacherInvitation} from '../entities/classes/teacher-invitation.entity'; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { fetchTeacher } from './teachers'; +import { getTeacherInvitationRepository } from '../data/repositories'; +import { mapToInvitation, mapToTeacherInvitationDTO } from '../interfaces/teacher-invitation'; +import { addClassTeacher, fetchClass } from './classes'; +import { TeacherInvitationData, TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; +import { ConflictException } from '../exceptions/conflict-exception'; +import { NotFoundException } from '../exceptions/not-found-exception'; +import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export async function getAllInvitations(username: string, sent: boolean): Promise { const teacher = await fetchTeacher(username); diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index c7f61773..38184d7d 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -5,11 +5,11 @@ import { createInvitationHandler, deleteInvitationHandler, getAllInvitationsHandler, - getInvitationHandler + getInvitationHandler, } from '../../src/controllers/teacher-invitations'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; import { getClassHandler } from '../../src/controllers/classes'; -import {BadRequestException} from "../../src/exceptions/bad-request-exception"; +import { BadRequestException } from '../../src/exceptions/bad-request-exception'; describe('Teacher controllers', () => { let req: Partial; @@ -91,8 +91,7 @@ describe('Teacher controllers', () => { params: { no: 'no params' }, }; - await expect( async () => getInvitationHandler(req as Request, res as Response)) - .rejects.toThrowError(BadRequestException); + await expect(async () => getInvitationHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); }); /* diff --git a/backend/tests/test_assets/classes/teacher-invitations.testdata.ts b/backend/tests/test_assets/classes/teacher-invitations.testdata.ts index 35a3d1b8..6337a513 100644 --- a/backend/tests/test_assets/classes/teacher-invitations.testdata.ts +++ b/backend/tests/test_assets/classes/teacher-invitations.testdata.ts @@ -1,36 +1,36 @@ -import {EntityManager} from '@mikro-orm/core'; -import {TeacherInvitation} from '../../../src/entities/classes/teacher-invitation.entity'; -import {Teacher} from '../../../src/entities/users/teacher.entity'; -import {Class} from '../../../src/entities/classes/class.entity'; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { EntityManager } from '@mikro-orm/core'; +import { TeacherInvitation } from '../../../src/entities/classes/teacher-invitation.entity'; +import { Teacher } from '../../../src/entities/users/teacher.entity'; +import { Class } from '../../../src/entities/classes/class.entity'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export function makeTestTeacherInvitations(em: EntityManager, teachers: Teacher[], classes: Class[]): TeacherInvitation[] { const teacherInvitation01 = em.create(TeacherInvitation, { sender: teachers[1], receiver: teachers[0], class: classes[1], - status: ClassStatus.Open + status: ClassStatus.Open, }); const teacherInvitation02 = em.create(TeacherInvitation, { sender: teachers[1], receiver: teachers[2], class: classes[1], - status: ClassStatus.Open + status: ClassStatus.Open, }); const teacherInvitation03 = em.create(TeacherInvitation, { sender: teachers[2], receiver: teachers[0], class: classes[2], - status: ClassStatus.Open + status: ClassStatus.Open, }); const teacherInvitation04 = em.create(TeacherInvitation, { sender: teachers[0], receiver: teachers[1], class: classes[0], - status: ClassStatus.Open + status: ClassStatus.Open, }); return [teacherInvitation01, teacherInvitation02, teacherInvitation03, teacherInvitation04]; diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index 37a4029f..0979bceb 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -1,5 +1,5 @@ import { UserDTO } from './user'; -import {ClassStatus} from "../util/class-join-request"; +import { ClassStatus } from '../util/class-join-request'; export interface TeacherInvitationDTO { sender: string | UserDTO; From ef04f6c7af638bb29e5e7f04d2c8cabf8cdf17d1 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Tue, 15 Apr 2025 17:33:03 +0200 Subject: [PATCH 62/71] fix: put route frontend + veranderingen delete --- .../controllers/teacher-invitations.test.ts | 26 +++++-------- common/src/interfaces/teacher-invitation.ts | 2 +- .../src/controllers/teacher-invitations.ts | 16 ++++++-- frontend/src/queries/teacher-invitations.ts | 38 +++++++++---------- 4 files changed, 40 insertions(+), 42 deletions(-) diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index 005dac26..c9e0d623 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -5,11 +5,12 @@ import { createInvitationHandler, deleteInvitationHandler, getAllInvitationsHandler, - getInvitationHandler + getInvitationHandler, updateInvitationHandler } from '../../src/controllers/teacher-invitations'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; import { getClassHandler } from '../../src/controllers/classes'; import {BadRequestException} from "../../src/exceptions/bad-request-exception"; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; describe('Teacher controllers', () => { let req: Partial; @@ -95,28 +96,19 @@ describe('Teacher controllers', () => { .rejects.toThrowError(BadRequestException); }); - /* - it('Create and accept invitation', async () => { + it('Accept invitation', async () => { const body = { sender: 'LimpBizkit', - receiver: 'testleerkracht1', + receiver: 'FooFighters', class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', } as TeacherInvitationData; req = { body }; - await createInvitationHandler(req as Request, res as Response); + await updateInvitationHandler(req as Request, res as Response); - req = { - params: { - sender: 'LimpBizkit', - receiver: 'testleerkracht1', - classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', - }, - body: { accepted: 'true' }, - }; - - await deleteInvitationHandler(req as Request, res as Response); + const result1 = jsonMock.mock.lastCall?.[0]; + expect(result1.invitation.status).toEqual(ClassStatus.Accepted); req = { params: { @@ -127,8 +119,8 @@ describe('Teacher controllers', () => { await getClassHandler(req as Request, res as Response); const result = jsonMock.mock.lastCall?.[0]; - expect(result.class.teachers).toContain('testleerkracht1'); + expect(result.class.teachers).toContain('FooFighters'); }); - */ + }); diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index 37a4029f..0d310d0e 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -12,5 +12,5 @@ export interface TeacherInvitationData { sender: string; receiver: string; class: string; - accepted?: boolean; + accepted?: boolean; // use for put requests, else skip } diff --git a/frontend/src/controllers/teacher-invitations.ts b/frontend/src/controllers/teacher-invitations.ts index a6e2045a..8083c152 100644 --- a/frontend/src/controllers/teacher-invitations.ts +++ b/frontend/src/controllers/teacher-invitations.ts @@ -14,15 +14,23 @@ export class TeacherInvitationController extends BaseController { super("teachers/invitations"); } - async getAll(username: string, by: boolean): Promise { - return this.get(`/${username}`, { by }); + async getAll(username: string, sent: boolean): Promise { + return this.get(`/${username}`, { sent }); + } + + async getBy(data: TeacherInvitationData): Promise { + return this.get(`/${data.sender}/${data.receiver}/${data.class}`) } async create(data: TeacherInvitationData): Promise { return this.post("/", data); } - async respond(data: TeacherInvitationData, accepted: boolean): Promise { - return this.delete(`/${data.sender}/${data.receiver}/${data.class}`, { accepted }); + async remove(data: TeacherInvitationData): Promise { + return this.delete(`/${data.sender}/${data.receiver}/${data.class}`); + } + + async respond(data: TeacherInvitationData) { + return this.put("/", data); } } diff --git a/frontend/src/queries/teacher-invitations.ts b/frontend/src/queries/teacher-invitations.ts index a56ea9cd..59357c32 100644 --- a/frontend/src/queries/teacher-invitations.ts +++ b/frontend/src/queries/teacher-invitations.ts @@ -6,15 +6,15 @@ import { type TeacherInvitationResponse, type TeacherInvitationsResponse, } from "@/controllers/teacher-invitations.ts"; -import type { TeacherInvitationData } from "@dwengo-1/common/dist/interfaces/teacher-invitation.ts"; -import type { TeacherDTO } from "@dwengo-1/common/dist/interfaces/teacher.ts"; +import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation"; +import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; const controller = new TeacherInvitationController(); /** - All the invitations the teacher send + All the invitations the teacher sent **/ -export function useTeacherInvitationsByQuery( +export function useTeacherInvitationsSentQuery( username: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ @@ -24,9 +24,9 @@ export function useTeacherInvitationsByQuery( } /** - All the pending invitations send to this teacher + All the pending invitations sent to this teacher */ -export function useTeacherInvitationsForQuery( +export function useTeacherInvitationsReceivedQuery( username: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ @@ -35,6 +35,15 @@ export function useTeacherInvitationsForQuery( }); } +export function useTeacherInvitationQuery( + data: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryFn: computed(async () => controller.getBy(toValue(data))), + enabled: () => Boolean(toValue(data)), + }); +} + export function useCreateTeacherInvitationMutation(): UseMutationReturnType< TeacherInvitationResponse, Error, @@ -46,25 +55,14 @@ export function useCreateTeacherInvitationMutation(): UseMutationReturnType< }); } -export function useAcceptTeacherInvitationMutation(): UseMutationReturnType< +export function useRespondTeacherInvitationMutation(): UseMutationReturnType< TeacherInvitationResponse, Error, TeacherDTO, unknown > { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, true), - }); -} - -export function useDeclineTeacherInvitationMutation(): UseMutationReturnType< - TeacherInvitationResponse, - Error, - TeacherDTO, - unknown -> { - return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false), + mutationFn: async (data: TeacherInvitationData) => controller.respond(data), }); } @@ -75,6 +73,6 @@ export function useDeleteTeacherInvitationMutation(): UseMutationReturnType< unknown > { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false), + mutationFn: async (data: TeacherInvitationData) => controller.remove(data), }); } From d84e6b485e76dbb38c8ad67223026b591434f1b7 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Tue, 15 Apr 2025 17:36:13 +0200 Subject: [PATCH 63/71] fix: lint --- backend/tests/controllers/teacher-invitations.test.ts | 2 +- common/src/interfaces/teacher-invitation.ts | 2 +- frontend/src/controllers/teacher-invitations.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index c9e0d623..f14d3b3a 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -37,7 +37,7 @@ describe('Teacher controllers', () => { expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; - console.log(result.invitations); + // console.log(result.invitations); expect(result.invitations).to.have.length.greaterThan(0); }); diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index 0d310d0e..3158b806 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -12,5 +12,5 @@ export interface TeacherInvitationData { sender: string; receiver: string; class: string; - accepted?: boolean; // use for put requests, else skip + accepted?: boolean; // Use for put requests, else skip } diff --git a/frontend/src/controllers/teacher-invitations.ts b/frontend/src/controllers/teacher-invitations.ts index 8083c152..f3383395 100644 --- a/frontend/src/controllers/teacher-invitations.ts +++ b/frontend/src/controllers/teacher-invitations.ts @@ -30,7 +30,7 @@ export class TeacherInvitationController extends BaseController { return this.delete(`/${data.sender}/${data.receiver}/${data.class}`); } - async respond(data: TeacherInvitationData) { + async respond(data: TeacherInvitationData): Promise { return this.put("/", data); } } From 447aeee9f3b504ec1bbea2b29226cc9394293908 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Tue, 15 Apr 2025 15:44:17 +0000 Subject: [PATCH 64/71] style: fix linting issues met ESLint --- backend/tests/controllers/teacher-invitations.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index f14d3b3a..2e9b594f 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -37,7 +37,7 @@ describe('Teacher controllers', () => { expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; - // console.log(result.invitations); + // Console.log(result.invitations); expect(result.invitations).to.have.length.greaterThan(0); }); From 63045c4223571cb84615111be9672c46f413277a Mon Sep 17 00:00:00 2001 From: Lint Action Date: Tue, 15 Apr 2025 15:44:22 +0000 Subject: [PATCH 65/71] style: fix linting issues met Prettier --- .../tests/controllers/teacher-invitations.test.ts | 13 +++++-------- frontend/src/controllers/teacher-invitations.ts | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index 2e9b594f..ed2f5ebf 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -5,12 +5,13 @@ import { createInvitationHandler, deleteInvitationHandler, getAllInvitationsHandler, - getInvitationHandler, updateInvitationHandler + getInvitationHandler, + updateInvitationHandler, } from '../../src/controllers/teacher-invitations'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; import { getClassHandler } from '../../src/controllers/classes'; -import {BadRequestException} from "../../src/exceptions/bad-request-exception"; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { BadRequestException } from '../../src/exceptions/bad-request-exception'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; describe('Teacher controllers', () => { let req: Partial; @@ -92,11 +93,9 @@ describe('Teacher controllers', () => { params: { no: 'no params' }, }; - await expect( async () => getInvitationHandler(req as Request, res as Response)) - .rejects.toThrowError(BadRequestException); + await expect(async () => getInvitationHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); }); - it('Accept invitation', async () => { const body = { sender: 'LimpBizkit', @@ -121,6 +120,4 @@ describe('Teacher controllers', () => { const result = jsonMock.mock.lastCall?.[0]; expect(result.class.teachers).toContain('FooFighters'); }); - - }); diff --git a/frontend/src/controllers/teacher-invitations.ts b/frontend/src/controllers/teacher-invitations.ts index f3383395..7750dea5 100644 --- a/frontend/src/controllers/teacher-invitations.ts +++ b/frontend/src/controllers/teacher-invitations.ts @@ -19,7 +19,7 @@ export class TeacherInvitationController extends BaseController { } async getBy(data: TeacherInvitationData): Promise { - return this.get(`/${data.sender}/${data.receiver}/${data.class}`) + return this.get(`/${data.sender}/${data.receiver}/${data.class}`); } async create(data: TeacherInvitationData): Promise { From 827a2f0e71ef450f313967e554737a13a7ba2c2e Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 17 Apr 2025 09:07:11 +0200 Subject: [PATCH 66/71] fix: .js toevoegen aan imports --- backend/src/controllers/teacher-invitations.ts | 4 ++-- backend/src/interfaces/group.ts | 2 +- backend/src/interfaces/submission.ts | 6 +++--- backend/src/interfaces/teacher-invitation.ts | 6 +++--- backend/src/routes/teacher-invitations.ts | 2 +- backend/src/services/questions.ts | 2 +- backend/src/services/teacher-invitations.ts | 14 +++++++------- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index 4956f3e2..5f003f7f 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; -import { requireFields } from './error-helper'; -import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations'; +import { requireFields } from './error-helper.js'; +import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; export async function getAllInvitationsHandler(req: Request, res: Response): Promise { diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts index 2a8287a3..792086d4 100644 --- a/backend/src/interfaces/group.ts +++ b/backend/src/interfaces/group.ts @@ -8,7 +8,7 @@ import { getGroupRepository } from '../data/repositories.js'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { Class } from '../entities/classes/class.entity.js'; import { StudentDTO } from '@dwengo-1/common/interfaces/student'; -import { mapToClassDTO } from './class'; +import { mapToClassDTO } from './class.js'; export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { const assignmentDto = groupDto.assignment as AssignmentDTO; diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts index aa88f4a1..e179d458 100644 --- a/backend/src/interfaces/submission.ts +++ b/backend/src/interfaces/submission.ts @@ -2,9 +2,9 @@ import { Submission } from '../entities/assignments/submission.entity.js'; import { mapToGroupDTO } from './group.js'; import { mapToStudentDTO } from './student.js'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; -import { getSubmissionRepository } from '../data/repositories'; -import { Student } from '../entities/users/student.entity'; -import { Group } from '../entities/assignments/group.entity'; +import { getSubmissionRepository } from '../data/repositories.js'; +import { Student } from '../entities/users/student.entity.js'; +import { Group } from '../entities/assignments/group.entity.js'; export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { return { diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index 8fef17af..88b66f7a 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -1,9 +1,9 @@ import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; import { mapToUserDTO } from './user.js'; import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; -import { getTeacherInvitationRepository } from '../data/repositories'; -import { Teacher } from '../entities/users/teacher.entity'; -import { Class } from '../entities/classes/class.entity'; +import { getTeacherInvitationRepository } from '../data/repositories.js'; +import { Teacher } from '../entities/users/teacher.entity.js'; +import { Class } from '../entities/classes/class.entity.js'; import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts index 772b1351..23b943d0 100644 --- a/backend/src/routes/teacher-invitations.ts +++ b/backend/src/routes/teacher-invitations.ts @@ -5,7 +5,7 @@ import { getAllInvitationsHandler, getInvitationHandler, updateInvitationHandler, -} from '../controllers/teacher-invitations'; +} from '../controllers/teacher-invitations.js'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 49bf9e92..a16c277b 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -10,7 +10,7 @@ import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; import { mapToAssignment } from '../interfaces/assignment.js'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { fetchStudent } from './students.js'; -import { NotFoundException } from '../exceptions/not-found-exception'; +import { NotFoundException } from '../exceptions/not-found-exception.js'; import { FALLBACK_VERSION_NUM } from '../config.js'; export async function getQuestionsAboutLearningObjectInAssignment( diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts index 07f61bae..aead8715 100644 --- a/backend/src/services/teacher-invitations.ts +++ b/backend/src/services/teacher-invitations.ts @@ -1,11 +1,11 @@ -import { fetchTeacher } from './teachers'; -import { getTeacherInvitationRepository } from '../data/repositories'; -import { mapToInvitation, mapToTeacherInvitationDTO } from '../interfaces/teacher-invitation'; -import { addClassTeacher, fetchClass } from './classes'; +import { fetchTeacher } from './teachers.js'; +import { getTeacherInvitationRepository } from '../data/repositories.js'; +import { mapToInvitation, mapToTeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; +import { addClassTeacher, fetchClass } from './classes.js'; import { TeacherInvitationData, TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; -import { ConflictException } from '../exceptions/conflict-exception'; -import { NotFoundException } from '../exceptions/not-found-exception'; -import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity'; +import { ConflictException } from '../exceptions/conflict-exception.js'; +import { NotFoundException } from '../exceptions/not-found-exception.js'; +import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export async function getAllInvitations(username: string, sent: boolean): Promise { From cda07055dde73558c91ea9f436cb80607d68ac23 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 17 Apr 2025 09:08:04 +0200 Subject: [PATCH 67/71] chore(frontend): Verberg Disccusions Verberg knop uit menubalk, luister niet naar endpoint --- frontend/src/components/MenuBar.vue | 15 ++++++++------- frontend/src/router/index.ts | 11 ++++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/MenuBar.vue b/frontend/src/components/MenuBar.vue index 328d607a..1240099d 100644 --- a/frontend/src/components/MenuBar.vue +++ b/frontend/src/components/MenuBar.vue @@ -80,13 +80,14 @@ > {{ t("classes") }} - - {{ t("discussions") }} - + + + + + + + +