From 12c1505ba79454f65b3d2ce6b04b07e62b68de5d Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Sun, 6 Apr 2025 23:08:11 +0200 Subject: [PATCH 01/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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; }