Merge branch 'dev' into feat/leerpad-vragen

This commit is contained in:
Timo De Meyst 2025-04-24 21:10:15 +02:00 committed by GitHub
commit 8240059c2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 10729 additions and 1042 deletions

View file

@ -13,8 +13,10 @@ DWENGO_DB_NAME=":memory:"
DWENGO_DB_UPDATE=true
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:9876,*

View file

@ -14,7 +14,8 @@
"format-check": "prettier --check src/",
"lint": "eslint . --fix",
"pretest:unit": "tsx ../docs/api/generate.ts && npm run build",
"test:unit": "vitest --run"
"test:unit": "vitest --run",
"test:coverage": "vitest --run --coverage.enabled true"
},
"dependencies": {
"@mikro-orm/core": "6.4.12",
@ -27,7 +28,6 @@
"cross": "^1.0.0",
"cross-env": "^7.0.3",
"dotenv": "^16.4.7",
"dwengo-1-common": "^0.1.1",
"express": "^5.0.1",
"express-jwt": "^8.5.1",
"gift-pegjs": "^1.0.2",

View file

@ -4,6 +4,7 @@ import {
deleteAssignment,
getAllAssignments,
getAssignment,
getAssignmentsQuestions,
getAssignmentsSubmissions,
putAssignment,
} from '../services/assignments.js';
@ -13,6 +14,19 @@ import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { EntityDTO } from '@mikro-orm/core';
function getAssignmentParams(req: Request): { classid: string; assignmentNumber: number; full: boolean } {
const classid = req.params.classid;
const assignmentNumber = Number(req.params.id);
const full = req.query.full === 'true';
requireFields({ assignmentNumber, classid });
if (isNaN(assignmentNumber)) {
throw new BadRequestException('Assignment id should be a number');
}
return { classid, assignmentNumber, full };
}
export async function getAllAssignmentsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid;
const full = req.query.full === 'true';
@ -38,57 +52,42 @@ export async function createAssignmentHandler(req: Request, res: Response): Prom
}
export async function getAssignmentHandler(req: Request, res: Response): Promise<void> {
const id = Number(req.params.id);
const classid = req.params.classid;
requireFields({ id, classid });
const { classid, assignmentNumber } = getAssignmentParams(req);
if (isNaN(id)) {
throw new BadRequestException('Assignment id should be a number');
}
const assignment = await getAssignment(classid, id);
const assignment = await getAssignment(classid, assignmentNumber);
res.json({ assignment });
}
export async function putAssignmentHandler(req: Request, res: Response): Promise<void> {
const id = Number(req.params.id);
const classid = req.params.classid;
requireFields({ id, classid });
if (isNaN(id)) {
throw new BadRequestException('Assignment id should be a number');
}
const { classid, assignmentNumber } = getAssignmentParams(req);
const assignmentData = req.body as Partial<EntityDTO<Assignment>>;
const assignment = await putAssignment(classid, id, assignmentData);
const assignment = await putAssignment(classid, assignmentNumber, assignmentData);
res.json({ assignment });
}
export async function deleteAssignmentHandler(req: Request, _res: Response): Promise<void> {
const id = Number(req.params.id);
const classid = req.params.classid;
requireFields({ id, classid });
export async function deleteAssignmentHandler(req: Request, res: Response): Promise<void> {
const { classid, assignmentNumber } = getAssignmentParams(req);
if (isNaN(id)) {
throw new BadRequestException('Assignment id should be a number');
}
const assignment = await deleteAssignment(classid, assignmentNumber);
await deleteAssignment(classid, id);
res.json({ assignment });
}
export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid;
const assignmentNumber = Number(req.params.id);
const full = req.query.full === 'true';
requireFields({ assignmentNumber, classid });
if (isNaN(assignmentNumber)) {
throw new BadRequestException('Assignment id should be a number');
}
const { classid, assignmentNumber, full } = getAssignmentParams(req);
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full);
res.json({ submissions });
}
export async function getAssignmentQuestionsHandler(req: Request, res: Response): Promise<void> {
const { classid, assignmentNumber, full } = getAssignmentParams(req);
const questions = await getAssignmentsQuestions(classid, assignmentNumber, full);
res.json({ questions });
}

View file

@ -1,5 +1,5 @@
import { Request, Response } from 'express';
import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupSubmissions, putGroup } from '../services/groups.js';
import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupQuestions, getGroupSubmissions, putGroup } from '../services/groups.js';
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
import { requireFields } from './error-helper.js';
import { BadRequestException } from '../exceptions/bad-request-exception.js';
@ -84,7 +84,7 @@ export async function createGroupHandler(req: Request, res: Response): Promise<v
res.status(201).json({ group });
}
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
function getGroupParams(req: Request): { classId: string; assignmentId: number; groupId: number; full: boolean } {
const classId = req.params.classid;
const assignmentId = Number(req.params.assignmentid);
const groupId = Number(req.params.groupid);
@ -100,7 +100,21 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P
throw new BadRequestException('Group id must be a number');
}
return { classId, assignmentId, groupId, full };
}
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
const { classId, assignmentId, groupId, full } = getGroupParams(req);
const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full);
res.json({ submissions });
}
export async function getGroupQuestionsHandler(req: Request, res: Response): Promise<void> {
const { classId, assignmentId, groupId, full } = getGroupParams(req);
const questions = await getGroupQuestions(classId, assignmentId, groupId, full);
res.json({ questions });
}

View file

@ -63,9 +63,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
public async findAllByAssignment(assignment: Assignment): Promise<Question[]> {
return this.find({
inGroup: {
$contained: assignment.groups,
},
inGroup: assignment.groups.getItems(),
learningObjectHruid: assignment.learningPathHruid,
learningObjectLanguage: assignment.learningPathLanguage,
});
@ -78,6 +76,13 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
});
}
public async findAllByGroup(inGroup: Group): Promise<Question[]> {
return this.findAll({
where: { inGroup },
orderBy: { timestamp: 'DESC' },
});
}
/**
* 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.

View file

@ -1,4 +1,4 @@
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Cascade, 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';
@ -34,6 +34,7 @@ export class Assignment {
@OneToMany({
entity: () => Group,
mappedBy: 'assignment',
cascade: [Cascade.ALL],
})
groups: Collection<Group> = new Collection<Group>(this);
}

View file

@ -1,6 +1,6 @@
import { Student } from '../users/student.entity.js';
import { Group } from './group.entity.js';
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
import { Entity, Enum, ManyToOne, PrimaryKey, Property, Cascade } from '@mikro-orm/core';
import { SubmissionRepository } from '../../data/assignments/submission-repository.js';
import { Language } from '@dwengo-1/common/util/language';
@ -21,8 +21,8 @@ export class Submission {
@PrimaryKey({ type: 'numeric', autoincrement: false })
learningObjectVersion = 1;
@ManyToOne({
entity: () => Group,
@ManyToOne(() => Group, {
cascade: [Cascade.REMOVE],
})
onBehalfOf!: Group;

View file

@ -4,6 +4,7 @@ import {
deleteAssignmentHandler,
getAllAssignmentsHandler,
getAssignmentHandler,
getAssignmentQuestionsHandler,
getAssignmentsSubmissionsHandler,
putAssignmentHandler,
} from '../controllers/assignments.js';
@ -23,6 +24,8 @@ router.delete('/:id', deleteAssignmentHandler);
router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
router.get('/:id/questions', getAssignmentQuestionsHandler);
router.use('/:assignmentid/groups', groupRouter);
export default router;

View file

@ -4,6 +4,7 @@ import {
deleteGroupHandler,
getAllGroupsHandler,
getGroupHandler,
getGroupQuestionsHandler,
getGroupSubmissionsHandler,
putGroupHandler,
} from '../controllers/groups.js';
@ -23,4 +24,6 @@ router.delete('/:groupid', deleteGroupHandler);
router.get('/:groupid/submissions', getGroupSubmissionsHandler);
router.get('/:groupid/questions', getGroupQuestionsHandler);
export default router;

View file

@ -1,5 +1,5 @@
import { EntityDTO } from '@mikro-orm/core';
import { getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { getGroupRepository, getQuestionRepository, getSubmissionRepository } from '../data/repositories.js';
import { Group } from '../entities/assignments/group.entity.js';
import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
@ -12,6 +12,8 @@ import { fetchClass } from './classes.js';
import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { Student } from '../entities/users/student.entity.js';
import { Class } from '../entities/classes/class.entity.js';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
async function assertMembersInClass(members: Student[], cls: Class): Promise<void> {
if (!members.every((student) => cls.students.contains(student))) {
@ -121,3 +123,21 @@ export async function getGroupSubmissions(
return submissions.map(mapToSubmissionDTOId);
}
export async function getGroupQuestions(
classId: string,
assignmentNumber: number,
groupNumber: number,
full: boolean
): Promise<QuestionDTO[] | QuestionId[]> {
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
const questionRepository = getQuestionRepository();
const questions = await questionRepository.findAllByGroup(group);
if (full) {
return questions.map(mapToQuestionDTO);
}
return questions.map(mapToQuestionDTOId);
}

View file

@ -17,6 +17,7 @@ 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 { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata';
describe('SubmissionRepository', () => {
let submissionRepository: SubmissionRepository;
@ -106,7 +107,7 @@ describe('SubmissionRepository', () => {
});
it('should not find a deleted submission', async () => {
const id = new LearningObjectIdentifier('id01', Language.English, 1);
const id = new LearningObjectIdentifier(testLearningObject01.hruid, testLearningObject01.language, testLearningObject01.version);
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1);
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);

View file

@ -5,5 +5,17 @@ export default defineConfig({
environment: 'node',
globals: true,
testTimeout: 100000,
coverage: {
reporter: ['text', 'json-summary', 'json'],
// If you want a coverage reports even if your tests are failing, include the reportOnFailure option
reportOnFailure: true,
exclude: ['**/*config*', '**/tests/**', 'src/*.ts', '**/dist/**', '**/node_modules/**', 'src/logging/**', 'src/routes/**'],
thresholds: {
lines: 50,
branches: 50,
functions: 50,
statements: 50,
},
},
},
});