Merge branch 'dev' into feat/assignment-page

# Conflicts:
#	backend/package.json
#	common/src/interfaces/assignment.ts
#	frontend/src/controllers/learning-paths.ts
#	frontend/src/i18n/locale/de.json
#	frontend/src/i18n/locale/en.json
#	frontend/src/i18n/locale/fr.json
#	frontend/src/i18n/locale/nl.json
#	frontend/src/views/assignments/CreateAssignment.vue
#	package-lock.json
This commit is contained in:
Joyelle Ndagijimana 2025-04-19 16:58:08 +02:00
commit a421b1996a
123 changed files with 2428 additions and 2658 deletions

View file

@ -16,12 +16,11 @@
"test:unit": "vitest --run" "test:unit": "vitest --run"
}, },
"dependencies": { "dependencies": {
"@dwengo-1/common": "^0.1.1", "@mikro-orm/core": "6.4.12",
"@mikro-orm/core": "6.4.9", "@mikro-orm/knex": "6.4.12",
"@mikro-orm/knex": "6.4.9", "@mikro-orm/postgresql": "6.4.12",
"@mikro-orm/postgresql": "6.4.9", "@mikro-orm/reflection": "6.4.12",
"@mikro-orm/reflection": "6.4.9", "@mikro-orm/sqlite": "6.4.12",
"@mikro-orm/sqlite": "6.4.9",
"axios": "^1.8.2", "axios": "^1.8.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross": "^1.0.0", "cross": "^1.0.0",
@ -44,7 +43,7 @@
"winston-loki": "^6.1.3" "winston-loki": "^6.1.3"
}, },
"devDependencies": { "devDependencies": {
"@mikro-orm/cli": "6.4.9", "@mikro-orm/cli": "6.4.12",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

View file

@ -69,8 +69,8 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise<
export async function createGroupHandler(req: Request, res: Response): Promise<void> { export async function createGroupHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid; const classid = req.params.classid;
const assignmentId = Number(req.params.assignmentid); const assignmentId = Number(req.params.assignmentid);
const members = req.body.members;
requireFields({ classid, assignmentId }); requireFields({ classid, assignmentId, members });
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
throw new BadRequestException('Assignment id must be a number'); throw new BadRequestException('Assignment id must be a number');

View file

@ -3,13 +3,10 @@ import { themes } from '../data/themes.js';
import { FALLBACK_LANG } from '../config.js'; import { FALLBACK_LANG } from '../config.js';
import learningPathService from '../services/learning-paths/learning-path-service.js'; import learningPathService from '../services/learning-paths/learning-path-service.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {
PersonalizationTarget,
personalizedForGroup,
personalizedForStudent,
} from '../services/learning-paths/learning-path-personalization-util.js';
import { BadRequestException } from '../exceptions/bad-request-exception.js'; import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { NotFoundException } from '../exceptions/not-found-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js';
import { Group } from '../entities/assignments/group.entity.js';
import { getAssignmentRepository, getGroupRepository } from '../data/repositories.js';
/** /**
* Fetch learning paths based on query parameters. * Fetch learning paths based on query parameters.
@ -20,20 +17,20 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
const searchQuery = req.query.search as string; const searchQuery = req.query.search as string;
const language = (req.query.language as string) || FALLBACK_LANG; const language = (req.query.language as string) || FALLBACK_LANG;
const forStudent = req.query.forStudent as string;
const forGroupNo = req.query.forGroup as string; const forGroupNo = req.query.forGroup as string;
const assignmentNo = req.query.assignmentNo as string; const assignmentNo = req.query.assignmentNo as string;
const classId = req.query.classId as string; const classId = req.query.classId as string;
let personalizationTarget: PersonalizationTarget | undefined; let forGroup: Group | undefined;
if (forStudent) { if (forGroupNo) {
personalizationTarget = await personalizedForStudent(forStudent);
} else if (forGroupNo) {
if (!assignmentNo || !classId) { if (!assignmentNo || !classId) {
throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.'); throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.');
} }
personalizationTarget = await personalizedForGroup(classId, parseInt(assignmentNo), parseInt(forGroupNo)); const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo));
if (assignment) {
forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined;
}
} }
let hruidList; let hruidList;
@ -48,18 +45,13 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
throw new NotFoundException(`Theme "${themeKey}" not found.`); throw new NotFoundException(`Theme "${themeKey}" not found.`);
} }
} else if (searchQuery) { } else if (searchQuery) {
const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, personalizationTarget); const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup);
res.json(searchResults); res.json(searchResults);
return; return;
} else { } else {
hruidList = themes.flatMap((theme) => theme.hruids); hruidList = themes.flatMap((theme) => theme.hruids);
} }
const learningPaths = await learningPathService.fetchLearningPaths( const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup);
hruidList,
language as Language,
`HRUIDs: ${hruidList.join(', ')}`,
personalizationTarget
);
res.json(learningPaths.data); res.json(learningPaths.data);
} }

View file

@ -17,15 +17,18 @@ export async function getSubmissionsHandler(req: Request, res: Response): Promis
const lang = languageMap[req.query.language as string] || Language.Dutch; const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = parseInt(req.query.version as string) ?? 1; const version = parseInt(req.query.version as string) ?? 1;
const submissions = await getSubmissionsForLearningObjectAndAssignment( const forGroup = req.query.forGroup as string | undefined;
const submissions: SubmissionDTO[] = await getSubmissionsForLearningObjectAndAssignment(
loHruid, loHruid,
lang, lang,
version, version,
req.query.classId as string, req.query.classId as string,
parseInt(req.query.assignmentId as string) parseInt(req.query.assignmentId as string),
forGroup ? parseInt(forGroup) : undefined
); );
res.json(submissions); res.json({ submissions });
} }
export async function getSubmissionHandler(req: Request, res: Response): Promise<void> { export async function getSubmissionHandler(req: Request, res: Response): Promise<void> {

View file

@ -4,7 +4,7 @@ import { Class } from '../../entities/classes/class.entity.js';
export class AssignmentRepository extends DwengoEntityRepository<Assignment> { export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> { public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
return this.findOne({ within: within, id: id }); return this.findOne({ within: within, id: id }, { populate: ['groups', 'groups.members'] });
} }
public async findByClassIdAndAssignmentId(withinClass: string, id: number): Promise<Assignment | null> { public async findByClassIdAndAssignmentId(withinClass: string, id: number): Promise<Assignment | null> {
return this.findOne({ within: { classId: withinClass }, id: id }); return this.findOne({ within: { classId: withinClass }, id: id });
@ -23,7 +23,7 @@ export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
}); });
} }
public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
return this.findAll({ where: { within: within } }); return this.findAll({ where: { within: within }, populate: ['groups', 'groups.members'] });
} }
public async deleteByClassAndId(within: Class, id: number): Promise<void> { public async deleteByClassAndId(within: Class, id: number): Promise<void> {
return this.deleteWhere({ within: within, id: id }); return this.deleteWhere({ within: within, id: id });

View file

@ -61,32 +61,30 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
/** /**
* Looks up all submissions for the given learning object which were submitted as part of the given assignment. * 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( public async findAllSubmissionsForLearningObjectAndAssignment(loId: LearningObjectIdentifier, assignment: Assignment): Promise<Submission[]> {
loId: LearningObjectIdentifier,
assignment: Assignment,
forStudentUsername?: string
): Promise<Submission[]> {
const onBehalfOf = forStudentUsername
? {
assignment,
members: {
$some: {
username: forStudentUsername,
},
},
}
: {
assignment,
};
return this.findAll({ return this.findAll({
where: { where: {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,
learningObjectVersion: loId.version, learningObjectVersion: loId.version,
onBehalfOf, onBehalfOf: {
assignment,
},
},
});
}
/**
* Looks up all submissions for the given learning object which were submitted by the given group
*/
public async findAllSubmissionsForLearningObjectAndGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission[]> {
return this.findAll({
where: {
learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language,
learningObjectVersion: loId.version,
onBehalfOf: group,
}, },
}); });
} }

View file

@ -1,6 +1,10 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { LearningPath } from '../../entities/content/learning-path.entity.js'; import { LearningPath } from '../../entities/content/learning-path.entity.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
import { RequiredEntityData } from '@mikro-orm/core';
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
import { EntityAlreadyExistsException } from '../../exceptions/entity-already-exists-exception.js';
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
@ -23,4 +27,27 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
populate: ['nodes', 'nodes.transitions'], populate: ['nodes', 'nodes.transitions'],
}); });
} }
public createNode(nodeData: RequiredEntityData<LearningPathNode>): LearningPathNode {
return this.em.create(LearningPathNode, nodeData);
}
public createTransition(transitionData: RequiredEntityData<LearningPathTransition>): LearningPathTransition {
return this.em.create(LearningPathTransition, transitionData);
}
public async saveLearningPathNodesAndTransitions(
path: LearningPath,
nodes: LearningPathNode[],
transitions: LearningPathTransition[],
options?: { preventOverwrite?: boolean }
): Promise<void> {
if (options?.preventOverwrite && (await this.findOne(path))) {
throw new EntityAlreadyExistsException('A learning path with this hruid/language combination already exists.');
}
const em = this.getEntityManager();
await em.persistAndFlush(path);
await Promise.all(nodes.map(async (it) => em.persistAndFlush(it)));
await Promise.all(transitions.map(async (it) => em.persistAndFlush(it)));
}
} }

View file

@ -14,7 +14,7 @@ export class Assignment {
}) })
within!: Class; within!: Class;
@PrimaryKey({ type: 'number', autoincrement: true }) @PrimaryKey({ type: 'integer', autoincrement: true })
id?: number; id?: number;
@Property({ type: 'string' }) @Property({ type: 'string' })
@ -35,5 +35,5 @@ export class Assignment {
entity: () => Group, entity: () => Group,
mappedBy: 'assignment', mappedBy: 'assignment',
}) })
groups!: Collection<Group>; groups: Collection<Group> = new Collection<Group>(this);
} }

View file

@ -7,17 +7,23 @@ import { GroupRepository } from '../../data/assignments/group-repository.js';
repository: () => GroupRepository, repository: () => GroupRepository,
}) })
export class Group { export class Group {
/*
WARNING: Don't move the definition of groupNumber! If it does not come before the definition of assignment,
creating groups fails because of a MikroORM bug!
*/
@PrimaryKey({ type: 'integer', autoincrement: true })
groupNumber?: number;
@ManyToOne({ @ManyToOne({
entity: () => Assignment, entity: () => Assignment,
primary: true, primary: true,
}) })
assignment!: Assignment; assignment!: Assignment;
@PrimaryKey({ type: 'integer', autoincrement: true })
groupNumber?: number;
@ManyToMany({ @ManyToMany({
entity: () => Student, entity: () => Student,
owner: true,
inversedBy: 'groups',
}) })
members!: Collection<Student>; members: Collection<Student> = new Collection<Student>(this);
} }

View file

@ -6,6 +6,9 @@ import { Language } from '@dwengo-1/common/util/language';
@Entity({ repository: () => SubmissionRepository }) @Entity({ repository: () => SubmissionRepository })
export class Submission { export class Submission {
@PrimaryKey({ type: 'integer', autoincrement: true })
submissionNumber?: number;
@PrimaryKey({ type: 'string' }) @PrimaryKey({ type: 'string' })
learningObjectHruid!: string; learningObjectHruid!: string;
@ -15,12 +18,9 @@ export class Submission {
}) })
learningObjectLanguage!: Language; learningObjectLanguage!: Language;
@PrimaryKey({ type: 'numeric' }) @PrimaryKey({ type: 'numeric', autoincrement: false })
learningObjectVersion = 1; learningObjectVersion = 1;
@PrimaryKey({ type: 'integer', autoincrement: true })
submissionNumber?: number;
@ManyToOne({ @ManyToOne({
entity: () => Group, entity: () => Group,
}) })

View file

@ -14,9 +14,9 @@ export class Class {
@Property({ type: 'string' }) @Property({ type: 'string' })
displayName!: string; displayName!: string;
@ManyToMany(() => Teacher) @ManyToMany({ entity: () => Teacher, owner: true, inversedBy: 'classes' })
teachers!: Collection<Teacher>; teachers!: Collection<Teacher>;
@ManyToMany(() => Student) @ManyToMany({ entity: () => Student, owner: true, inversedBy: 'classes' })
students!: Collection<Student>; students!: Collection<Student>;
} }

View file

@ -1,4 +1,4 @@
import { Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { ArrayType, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Attachment } from './attachment.entity.js'; import { Attachment } from './attachment.entity.js';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
@ -42,7 +42,7 @@ export class LearningObject {
@Property({ type: 'array' }) @Property({ type: 'array' })
keywords: string[] = []; keywords: string[] = [];
@Property({ type: 'array', nullable: true }) @Property({ type: new ArrayType((i) => Number(i)), nullable: true })
targetAges?: number[] = []; targetAges?: number[] = [];
@Property({ type: 'bool' }) @Property({ type: 'bool' })

View file

@ -1,16 +1,16 @@
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core';
import { LearningPath } from './learning-path.entity.js'; import { LearningPath } from './learning-path.entity.js';
import { LearningPathTransition } from './learning-path-transition.entity.js'; import { LearningPathTransition } from './learning-path-transition.entity.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
@Entity() @Entity()
export class LearningPathNode { export class LearningPathNode {
@PrimaryKey({ type: 'integer', autoincrement: true })
nodeNumber?: number;
@ManyToOne({ entity: () => LearningPath, primary: true }) @ManyToOne({ entity: () => LearningPath, primary: true })
learningPath!: Rel<LearningPath>; learningPath!: Rel<LearningPath>;
@PrimaryKey({ type: 'integer', autoincrement: true })
nodeNumber!: number;
@Property({ type: 'string' }) @Property({ type: 'string' })
learningObjectHruid!: string; learningObjectHruid!: string;
@ -27,7 +27,7 @@ export class LearningPathNode {
startNode!: boolean; startNode!: boolean;
@OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' }) @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' })
transitions: LearningPathTransition[] = []; transitions!: Collection<LearningPathTransition>;
@Property({ length: 3 }) @Property({ length: 3 })
createdAt: Date = new Date(); createdAt: Date = new Date();

View file

@ -3,12 +3,12 @@ import { LearningPathNode } from './learning-path-node.entity.js';
@Entity() @Entity()
export class LearningPathTransition { export class LearningPathTransition {
@ManyToOne({ entity: () => LearningPathNode, primary: true })
node!: Rel<LearningPathNode>;
@PrimaryKey({ type: 'numeric' }) @PrimaryKey({ type: 'numeric' })
transitionNumber!: number; transitionNumber!: number;
@ManyToOne({ entity: () => LearningPathNode, primary: true })
node!: Rel<LearningPathNode>;
@Property({ type: 'string' }) @Property({ type: 'string' })
condition!: string; condition!: string;

View file

@ -1,4 +1,4 @@
import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; import { LearningPathRepository } from '../../data/content/learning-path-repository.js';
import { LearningPathNode } from './learning-path-node.entity.js'; import { LearningPathNode } from './learning-path-node.entity.js';
@ -25,5 +25,5 @@ export class LearningPath {
image: Buffer | null = null; image: Buffer | null = null;
@OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' }) @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' })
nodes: LearningPathNode[] = []; nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this);
} }

View file

@ -8,9 +8,9 @@ import { StudentRepository } from '../../data/users/student-repository.js';
repository: () => StudentRepository, repository: () => StudentRepository,
}) })
export class Student extends User { export class Student extends User {
@ManyToMany(() => Class) @ManyToMany({ entity: () => Class, mappedBy: 'students' })
classes!: Collection<Class>; classes!: Collection<Class>;
@ManyToMany(() => Group) @ManyToMany({ entity: () => Group, mappedBy: 'members' })
groups!: Collection<Group>; groups: Collection<Group> = new Collection<Group>(this);
} }

View file

@ -5,6 +5,6 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js';
@Entity({ repository: () => TeacherRepository }) @Entity({ repository: () => TeacherRepository })
export class Teacher extends User { export class Teacher extends User {
@ManyToMany(() => Class) @ManyToMany({ entity: () => Class, mappedBy: 'teachers' })
classes!: Collection<Class>; classes!: Collection<Class>;
} }

View file

@ -0,0 +1,12 @@
import { ExceptionWithHttpState } from './exception-with-http-state.js';
/**
* Exception for HTTP 500 Internal Server Error
*/
export class ServerErrorException extends ExceptionWithHttpState {
status = 500;
constructor(message = 'Internal server error, something went wrong') {
super(500, message);
}
}

View file

@ -1,18 +1,14 @@
import { languageMap } from '@dwengo-1/common/util/language'; import { languageMap } from '@dwengo-1/common/util/language';
import { FALLBACK_LANG } from '../config.js';
import { Assignment } from '../entities/assignments/assignment.entity.js'; import { Assignment } from '../entities/assignments/assignment.entity.js';
import { Class } from '../entities/classes/class.entity.js'; import { Class } from '../entities/classes/class.entity.js';
import { getLogger } from '../logging/initalize.js'; import { AssignmentDTO, AssignmentDTOId } from '@dwengo-1/common/interfaces/assignment';
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { mapToGroupDTO } from './group.js';
import { getAssignmentRepository } from '../data/repositories.js';
export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO { export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTOId {
return { return {
id: assignment.id!, id: assignment.id!,
within: assignment.within.classId!, within: assignment.within.classId!,
title: assignment.title,
description: assignment.description,
learningPath: assignment.learningPathHruid,
language: assignment.learningPathLanguage,
}; };
} }
@ -24,19 +20,17 @@ export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
description: assignment.description, description: assignment.description,
learningPath: assignment.learningPathHruid, learningPath: assignment.learningPathHruid,
language: assignment.learningPathLanguage, language: assignment.learningPathLanguage,
// Groups: assignment.groups.map(mapToGroupDTO), groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
}; };
} }
export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment { export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment {
const assignment = new Assignment(); return getAssignmentRepository().create({
assignment.title = assignmentData.title; within: cls,
assignment.description = assignmentData.description; title: assignmentData.title,
assignment.learningPathHruid = assignmentData.learningPath; description: assignmentData.description,
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; learningPathHruid: assignmentData.learningPath,
assignment.within = cls; learningPathLanguage: languageMap[assignmentData.language],
groups: [],
getLogger().debug(assignment); });
return assignment;
} }

View file

@ -1,14 +1,12 @@
import { Group } from '../entities/assignments/group.entity.js'; import { Group } from '../entities/assignments/group.entity.js';
import { mapToAssignment } from './assignment.js'; import { mapToAssignment } from './assignment.js';
import { mapToStudent } from './student.js'; import { mapToStudent } from './student.js';
import { mapToAssignmentDTO } from './assignment.js';
import { mapToStudentDTO } from './student.js'; import { mapToStudentDTO } from './student.js';
import { GroupDTO } from '@dwengo-1/common/interfaces/group'; import { GroupDTO, GroupDTOId } from '@dwengo-1/common/interfaces/group';
import { getGroupRepository } from '../data/repositories.js'; import { getGroupRepository } from '../data/repositories.js';
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
import { Class } from '../entities/classes/class.entity.js'; import { Class } from '../entities/classes/class.entity.js';
import { StudentDTO } from '@dwengo-1/common/interfaces/student'; import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { mapToClassDTO } from './class.js';
export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group {
const assignmentDto = groupDto.assignment as AssignmentDTO; const assignmentDto = groupDto.assignment as AssignmentDTO;
@ -20,18 +18,18 @@ export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group {
}); });
} }
export function mapToGroupDTO(group: Group): GroupDTO { export function mapToGroupDTO(group: Group, cls: Class): GroupDTO {
return { return {
class: mapToClassDTO(group.assignment.within), class: cls.classId!,
assignment: mapToAssignmentDTO(group.assignment), assignment: group.assignment.id!,
groupNumber: group.groupNumber!, groupNumber: group.groupNumber!,
members: group.members.map(mapToStudentDTO), members: group.members.map(mapToStudentDTO),
}; };
} }
export function mapToGroupDTOId(group: Group): GroupDTO { export function mapToGroupDTOId(group: Group, cls: Class): GroupDTOId {
return { return {
class: group.assignment.within.classId!, class: cls.classId!,
assignment: group.assignment.id!, assignment: group.assignment.id!,
groupNumber: group.groupNumber!, groupNumber: group.groupNumber!,
}; };

View file

@ -31,7 +31,7 @@ export function mapToQuestionDTO(question: Question): QuestionDTO {
learningObjectIdentifier, learningObjectIdentifier,
sequenceNumber: question.sequenceNumber!, sequenceNumber: question.sequenceNumber!,
author: mapToStudentDTO(question.author), author: mapToStudentDTO(question.author),
inGroup: mapToGroupDTOId(question.inGroup), inGroup: mapToGroupDTOId(question.inGroup, question.inGroup.assignment?.within),
timestamp: question.timestamp.toISOString(), timestamp: question.timestamp.toISOString(),
content: question.content, content: question.content,
}; };

View file

@ -1,5 +1,5 @@
import { Submission } from '../entities/assignments/submission.entity.js'; import { Submission } from '../entities/assignments/submission.entity.js';
import { mapToGroupDTO } from './group.js'; import { mapToGroupDTOId } from './group.js';
import { mapToStudentDTO } from './student.js'; import { mapToStudentDTO } from './student.js';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { getSubmissionRepository } from '../data/repositories.js'; import { getSubmissionRepository } from '../data/repositories.js';
@ -13,11 +13,10 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
language: submission.learningObjectLanguage, language: submission.learningObjectLanguage,
version: submission.learningObjectVersion, version: submission.learningObjectVersion,
}, },
submissionNumber: submission.submissionNumber, submissionNumber: submission.submissionNumber,
submitter: mapToStudentDTO(submission.submitter), submitter: mapToStudentDTO(submission.submitter),
time: submission.submissionTime, time: submission.submissionTime,
group: mapToGroupDTO(submission.onBehalfOf), group: submission.onBehalfOf ? mapToGroupDTOId(submission.onBehalfOf, submission.onBehalfOf.assignment.within) : undefined,
content: submission.content, content: submission.content,
}; };
} }

View file

@ -9,7 +9,7 @@ export function errorHandler(err: unknown, _req: Request, res: Response, _: Next
logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`); logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`);
res.status(err.status).json(err); res.status(err.status).json(err);
} else { } else {
logger.error(`Unexpected error occurred while handing a request: ${JSON.stringify(err)}`); logger.error(`Unexpected error occurred while handing a request: ${(err as { stack: string })?.stack ?? JSON.stringify(err)}`);
res.status(500).json(err); res.status(500).json(err);
} }
} }

View file

@ -5,7 +5,7 @@ const router = express.Router({ mergeParams: true });
// Root endpoint used to search objects // Root endpoint used to search objects
router.get('/', getSubmissionsHandler); router.get('/', getSubmissionsHandler);
router.post('/:id', createSubmissionHandler); router.post('/', createSubmissionHandler);
// Information about an submission with id 'id' // Information about an submission with id 'id'
router.get('/:id', getSubmissionHandler); router.get('/:id', getSubmissionHandler);

View file

@ -1,4 +1,4 @@
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { AssignmentDTO, AssignmentDTOId } from '@dwengo-1/common/interfaces/assignment';
import { import {
getAssignmentRepository, getAssignmentRepository,
getClassRepository, getClassRepository,
@ -16,6 +16,8 @@ import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { EntityDTO } from '@mikro-orm/core'; import { EntityDTO } from '@mikro-orm/core';
import { putObject } from './service-helper.js'; import { putObject } from './service-helper.js';
import { fetchStudents } from './students.js';
import { ServerErrorException } from '../exceptions/server-error-exception.js';
export async function fetchAssignment(classid: string, assignmentNumber: number): Promise<Assignment> { export async function fetchAssignment(classid: string, assignmentNumber: number): Promise<Assignment> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
@ -35,7 +37,7 @@ export async function fetchAssignment(classid: string, assignmentNumber: number)
return assignment; return assignment;
} }
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> { export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[] | AssignmentDTOId[]> {
const cls = await fetchClass(classid); const cls = await fetchClass(classid);
const assignmentRepository = getAssignmentRepository(); const assignmentRepository = getAssignmentRepository();
@ -51,13 +53,39 @@ export async function getAllAssignments(classid: string, full: boolean): Promise
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO> { export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO> {
const cls = await fetchClass(classid); const cls = await fetchClass(classid);
const assignment = mapToAssignment(assignmentData, cls);
const assignmentRepository = getAssignmentRepository(); const assignmentRepository = getAssignmentRepository();
const newAssignment = assignmentRepository.create(assignment); const assignment = mapToAssignment(assignmentData, cls);
await assignmentRepository.save(newAssignment, { preventOverwrite: true }); await assignmentRepository.save(assignment);
return mapToAssignmentDTO(newAssignment); if (assignmentData.groups) {
/*
For some reason when trying to add groups, it does not work when using the original assignment variable.
The assignment needs to be refetched in order for it to work.
*/
const assignmentCopy = await assignmentRepository.findByClassAndId(cls, assignment.id!);
if (assignmentCopy === null) {
throw new ServerErrorException('Something has gone horribly wrong. Could not find newly added assignment which is needed to add groups.');
}
const groupRepository = getGroupRepository();
(assignmentData.groups as string[][]).forEach(async (memberUsernames) => {
const members = await fetchStudents(memberUsernames);
const newGroup = groupRepository.create({
assignment: assignmentCopy,
members: members,
});
await groupRepository.save(newGroup);
});
}
/* Need to refetch the assignment here again such that the groups are added. */
const assignmentWithGroups = await fetchAssignment(classid, assignment.id!);
return mapToAssignmentDTO(assignmentWithGroups);
} }
export async function getAssignment(classid: string, id: number): Promise<AssignmentDTO> { export async function getAssignment(classid: string, id: number): Promise<AssignmentDTO> {

View file

@ -1,13 +1,14 @@
import { EntityDTO } from '@mikro-orm/core'; import { EntityDTO } from '@mikro-orm/core';
import { getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; import { getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { Group } from '../entities/assignments/group.entity.js'; import { Group } from '../entities/assignments/group.entity.js';
import { mapToGroupDTO, mapToShallowGroupDTO } from '../interfaces/group.js'; import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
import { GroupDTO } from '@dwengo-1/common/interfaces/group'; import { GroupDTO, GroupDTOId } from '@dwengo-1/common/interfaces/group';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { fetchAssignment } from './assignments.js'; import { fetchAssignment } from './assignments.js';
import { NotFoundException } from '../exceptions/not-found-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js';
import { putObject } from './service-helper.js'; import { putObject } from './service-helper.js';
import { fetchStudents } from './students.js';
export async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<Group> { export async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<Group> {
const assignment = await fetchAssignment(classId, assignmentNumber); const assignment = await fetchAssignment(classId, assignmentNumber);
@ -24,7 +25,7 @@ export async function fetchGroup(classId: string, assignmentNumber: number, grou
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> { export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> {
const group = await fetchGroup(classId, assignmentNumber, groupNumber); const group = await fetchGroup(classId, assignmentNumber, groupNumber);
return mapToGroupDTO(group); return mapToGroupDTO(group, group.assignment.within);
} }
export async function putGroup( export async function putGroup(
@ -37,7 +38,7 @@ export async function putGroup(
await putObject<Group>(group, groupData, getGroupRepository()); await putObject<Group>(group, groupData, getGroupRepository());
return mapToGroupDTO(group); return mapToGroupDTO(group, group.assignment.within);
} }
export async function deleteGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> { export async function deleteGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> {
@ -47,7 +48,7 @@ export async function deleteGroup(classId: string, assignmentNumber: number, gro
const groupRepository = getGroupRepository(); const groupRepository = getGroupRepository();
await groupRepository.deleteByAssignmentAndGroupNumber(assignment, groupNumber); await groupRepository.deleteByAssignmentAndGroupNumber(assignment, groupNumber);
return mapToGroupDTO(group); return mapToGroupDTO(group, assignment.within);
} }
export async function getExistingGroupFromGroupDTO(groupData: GroupDTO): Promise<Group> { export async function getExistingGroupFromGroupDTO(groupData: GroupDTO): Promise<Group> {
@ -59,12 +60,8 @@ export async function getExistingGroupFromGroupDTO(groupData: GroupDTO): Promise
} }
export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise<GroupDTO> { export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise<GroupDTO> {
const studentRepository = getStudentRepository();
const memberUsernames = (groupData.members as string[]) || []; const memberUsernames = (groupData.members as string[]) || [];
const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter( const members = await fetchStudents(memberUsernames);
(student) => student !== null
);
const assignment = await fetchAssignment(classid, assignmentNumber); const assignment = await fetchAssignment(classid, assignmentNumber);
@ -73,22 +70,23 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
assignment: assignment, assignment: assignment,
members: members, members: members,
}); });
await groupRepository.save(newGroup); await groupRepository.save(newGroup);
return mapToGroupDTO(newGroup); return mapToGroupDTO(newGroup, newGroup.assignment.within);
} }
export async function getAllGroups(classId: string, assignmentNumber: number, full: boolean): Promise<GroupDTO[]> { export async function getAllGroups(classId: string, assignmentNumber: number, full: boolean): Promise<GroupDTO[] | GroupDTOId[]> {
const assignment = await fetchAssignment(classId, assignmentNumber); const assignment = await fetchAssignment(classId, assignmentNumber);
const groupRepository = getGroupRepository(); const groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsForAssignment(assignment); const groups = await groupRepository.findAllGroupsForAssignment(assignment);
if (full) { if (full) {
return groups.map(mapToGroupDTO); return groups.map((group) => mapToGroupDTO(group, assignment.within));
} }
return groups.map(mapToShallowGroupDTO); return groups.map((group) => mapToGroupDTOId(group, assignment.within));
} }
export async function getGroupSubmissions( export async function getGroupSubmissions(

View file

@ -8,12 +8,13 @@ import {
LearningPathResponse, LearningPathResponse,
} from '@dwengo-1/common/interfaces/learning-content'; } from '@dwengo-1/common/interfaces/learning-content';
import { getLogger } from '../logging/initalize.js'; import { getLogger } from '../logging/initalize.js';
import { v4 } from 'uuid';
function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
return { return {
key: data.hruid, // Hruid learningObject (not path) key: data.hruid, // Hruid learningObject (not path)
_id: data._id, _id: data._id,
uuid: data.uuid, uuid: data.uuid || v4(),
version: data.version, version: data.version,
title: data.title, title: data.title,
htmlUrl, // Url to fetch html content htmlUrl, // Url to fetch html content

View file

@ -32,7 +32,7 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
educationalGoals: learningObject.educationalGoals, educationalGoals: learningObject.educationalGoals,
returnValue: { returnValue: {
callback_url: learningObject.returnValue.callbackUrl, callback_url: learningObject.returnValue.callbackUrl,
callback_schema: JSON.parse(learningObject.returnValue.callbackSchema), callback_schema: learningObject.returnValue.callbackSchema === '' ? '' : JSON.parse(learningObject.returnValue.callbackSchema),
}, },
skosConcepts: learningObject.skosConcepts, skosConcepts: learningObject.skosConcepts,
targetAges: learningObject.targetAges || [], targetAges: learningObject.targetAges || [],

View file

@ -11,6 +11,7 @@ import {
LearningPathIdentifier, LearningPathIdentifier,
LearningPathResponse, LearningPathResponse,
} from '@dwengo-1/common/interfaces/learning-content'; } from '@dwengo-1/common/interfaces/learning-content';
import { v4 } from 'uuid';
const logger: Logger = getLogger(); const logger: Logger = getLogger();
@ -23,7 +24,7 @@ function filterData(data: LearningObjectMetadata): FilteredLearningObject {
return { return {
key: data.hruid, // Hruid learningObject (not path) key: data.hruid, // Hruid learningObject (not path)
_id: data._id, _id: data._id,
uuid: data.uuid, uuid: data.uuid ?? v4(),
version: data.version, version: data.version,
title: data.title, title: data.title,
htmlUrl: `/learningObject/${data.hruid}/html?language=${data.language}&version=${data.version}`, // Url to fetch html content htmlUrl: `/learningObject/${data.hruid}/html?language=${data.language}&version=${data.version}`, // Url to fetch html content

View file

@ -38,7 +38,7 @@ class GiftProcessor extends StringProcessor {
let html = "<div class='learning-object-gift'>\n"; let html = "<div class='learning-object-gift'>\n";
let i = 1; let i = 1;
for (const question of quizQuestions) { for (const question of quizQuestions) {
html += ` <div class='gift-question' id='gift-q${i}'>\n`; html += ` <div class='gift-question gift-question-type-${question.type}' id='gift-q${i}'>\n`;
html += ' ' + this.renderQuestion(question, i).replaceAll(/\n(.+)/g, '\n $1'); // Replace for indentation. html += ' ' + this.renderQuestion(question, i).replaceAll(/\n(.+)/g, '\n $1'); // Replace for indentation.
html += ` </div>\n`; html += ` </div>\n`;
i++; i++;

View file

@ -14,7 +14,7 @@ export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<Multipl
for (const choice of question.choices) { for (const choice of question.choices) {
renderedHtml += `<div class="gift-choice-div">\n`; renderedHtml += `<div class="gift-choice-div">\n`;
renderedHtml += ` <input type='radio' id='gift-q${questionNumber}-choice-${i}' name='gift-q${questionNumber}-choices' value="${i}"/>\n`; renderedHtml += ` <input type='radio' id='gift-q${questionNumber}-choice-${i}' name='gift-q${questionNumber}-choices' value="${i}"/>\n`;
renderedHtml += ` <label for='gift-q${questionNumber}-choice-${i}'>${choice.text}</label>\n`; renderedHtml += ` <label for='gift-q${questionNumber}-choice-${i}'>${choice.text.text}</label>\n`;
renderedHtml += `</div>\n`; renderedHtml += `</div>\n`;
i++; i++;
} }

View file

@ -4,7 +4,7 @@ import { getLearningPathRepository } from '../../data/repositories.js';
import learningObjectService from '../learning-objects/learning-object-service.js'; import learningObjectService from '../learning-objects/learning-object-service.js';
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
import { getLastSubmissionForCustomizationTarget, isTransitionPossible, PersonalizationTarget } from './learning-path-personalization-util.js'; import { getLastSubmissionForGroup, isTransitionPossible } from './learning-path-personalization-util.js';
import { import {
FilteredLearningObject, FilteredLearningObject,
LearningObjectNode, LearningObjectNode,
@ -13,13 +13,16 @@ import {
Transition, Transition,
} from '@dwengo-1/common/interfaces/learning-content'; } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { Group } from '../../entities/assignments/group.entity';
import { Collection } from '@mikro-orm/core';
import { v4 } from 'uuid';
/** /**
* Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its * Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its
* corresponding learning object. * corresponding learning object.
* @param nodes The nodes to find the learning object for. * @param nodes The nodes to find the learning object for.
*/ */
async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Map<LearningPathNode, FilteredLearningObject>> { async function getLearningObjectsForNodes(nodes: Collection<LearningPathNode>): Promise<Map<LearningPathNode, FilteredLearningObject>> {
// Fetching the corresponding learning object for each of the nodes and creating a map that maps each node to // Fetching the corresponding learning object for each of the nodes and creating a map that maps each node to
// Its corresponding learning object. // Its corresponding learning object.
const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>( const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>(
@ -44,7 +47,7 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
/** /**
* Convert the given learning path entity to an object which conforms to the learning path content. * Convert the given learning path entity to an object which conforms to the learning path content.
*/ */
async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: PersonalizationTarget): Promise<LearningPath> { async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: Group): Promise<LearningPath> {
// Fetch the corresponding learning object for each node since some parts of the expected response contains parts // Fetch the corresponding learning object for each node since some parts of the expected response contains parts
// With information which is not available in the LearningPathNodes themselves. // With information which is not available in the LearningPathNodes themselves.
const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes); const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes);
@ -89,10 +92,10 @@ async function convertLearningPath(learningPath: LearningPathEntity, order: numb
async function convertNode( async function convertNode(
node: LearningPathNode, node: LearningPathNode,
learningObject: FilteredLearningObject, learningObject: FilteredLearningObject,
personalizedFor: PersonalizationTarget | undefined, personalizedFor: Group | undefined,
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>
): Promise<LearningObjectNode> { ): Promise<LearningObjectNode> {
const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null; const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(node, personalizedFor) : null;
const transitions = node.transitions const transitions = node.transitions
.filter( .filter(
(trans) => (trans) =>
@ -108,6 +111,7 @@ async function convertNode(
updatedAt: node.updatedAt.toISOString(), updatedAt: node.updatedAt.toISOString(),
learningobject_hruid: node.learningObjectHruid, learningobject_hruid: node.learningObjectHruid,
version: learningObject.version, version: learningObject.version,
done: personalizedFor ? lastSubmission !== null : undefined,
transitions, transitions,
}; };
} }
@ -121,7 +125,7 @@ async function convertNode(
*/ */
async function convertNodes( async function convertNodes(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>, nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget personalizedFor?: Group
): Promise<LearningObjectNode[]> { ): Promise<LearningObjectNode[]> {
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) =>
convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects) convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects)
@ -161,7 +165,7 @@ function convertTransition(
_id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path. _id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
default: false, // We don't work with default transitions but retain this for backwards compatibility. default: false, // We don't work with default transitions but retain this for backwards compatibility.
next: { next: {
_id: nextNode._id + index, // Construct a unique ID for the transition for backwards compatibility. _id: nextNode._id ? nextNode._id + index : v4(), // Construct a unique ID for the transition for backwards compatibility.
hruid: transition.next.learningObjectHruid, hruid: transition.next.learningObjectHruid,
language: nextNode.language, language: nextNode.language,
version: nextNode.version, version: nextNode.version,
@ -177,12 +181,7 @@ const databaseLearningPathProvider: LearningPathProvider = {
/** /**
* Fetch the learning paths with the given hruids from the database. * Fetch the learning paths with the given hruids from the database.
*/ */
async fetchLearningPaths( async fetchLearningPaths(hruids: string[], language: Language, source: string, personalizedFor?: Group): Promise<LearningPathResponse> {
hruids: string[],
language: Language,
source: string,
personalizedFor?: PersonalizationTarget
): Promise<LearningPathResponse> {
const learningPathRepo = getLearningPathRepository(); const learningPathRepo = getLearningPathRepository();
const learningPaths = (await Promise.all(hruids.map(async (hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter( const learningPaths = (await Promise.all(hruids.map(async (hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter(
@ -202,7 +201,7 @@ const databaseLearningPathProvider: LearningPathProvider = {
/** /**
* Search learning paths in the database using the given search string. * Search learning paths in the database using the given search string.
*/ */
async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> { async searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]> {
const learningPathRepo = getLearningPathRepository(); const learningPathRepo = getLearningPathRepository();
const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language); const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language);

View file

@ -1,76 +1,22 @@
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
import { Student } from '../../entities/users/student.entity.js';
import { Group } from '../../entities/assignments/group.entity.js'; import { Group } from '../../entities/assignments/group.entity.js';
import { Submission } from '../../entities/assignments/submission.entity.js'; import { Submission } from '../../entities/assignments/submission.entity.js';
import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../../data/repositories.js'; import { getSubmissionRepository } from '../../data/repositories.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
import { JSONPath } from 'jsonpath-plus'; import { JSONPath } from 'jsonpath-plus';
export type PersonalizationTarget = { type: 'student'; student: Student } | { type: 'group'; group: Group };
/** /**
* Shortcut function to easily create a PersonalizationTarget object for a student by his/her username. * Returns the last submission for the learning object associated with the given node and for the group
* @param username Username of the student we want to generate a personalized learning path for.
* If there is no student with this username, return undefined.
*/ */
export async function personalizedForStudent(username: string): Promise<PersonalizationTarget | undefined> { export async function getLastSubmissionForGroup(node: LearningPathNode, pathFor: Group): Promise<Submission | null> {
const student = await getStudentRepository().findByUsername(username);
if (student) {
return {
type: 'student',
student: student,
};
}
return undefined;
}
/**
* Shortcut function to easily create a PersonalizationTarget object for a group by class name, assignment number and
* group number.
* @param classId Id of the class in which this group was created
* @param assignmentNumber Number of the assignment for which this group was created
* @param groupNumber Number of the group for which we want to personalize the learning path.
*/
export async function personalizedForGroup(
classId: string,
assignmentNumber: number,
groupNumber: number
): Promise<PersonalizationTarget | undefined> {
const clazz = await getClassRepository().findById(classId);
if (!clazz) {
return undefined;
}
const group = await getGroupRepository().findOne({
assignment: {
within: clazz,
id: assignmentNumber,
},
groupNumber: groupNumber,
});
if (group) {
return {
type: 'group',
group: group,
};
}
return undefined;
}
/**
* Returns the last submission for the learning object associated with the given node and for the student or group
*/
export async function getLastSubmissionForCustomizationTarget(node: LearningPathNode, pathFor: PersonalizationTarget): Promise<Submission | null> {
const submissionRepo = getSubmissionRepository(); const submissionRepo = getSubmissionRepository();
const learningObjectId: LearningObjectIdentifier = { const learningObjectId: LearningObjectIdentifier = {
hruid: node.learningObjectHruid, hruid: node.learningObjectHruid,
language: node.language, language: node.language,
version: node.version, version: node.version,
}; };
if (pathFor.type === 'group') { return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor);
return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor.group);
}
return await submissionRepo.findMostRecentSubmissionForStudent(learningObjectId, pathFor.student);
} }
/** /**

View file

@ -1,6 +1,6 @@
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import { PersonalizationTarget } from './learning-path-personalization-util.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { Group } from '../../entities/assignments/group.entity';
/** /**
* Generic interface for a service which provides access to learning paths from a data source. * Generic interface for a service which provides access to learning paths from a data source.
@ -9,10 +9,10 @@ export interface LearningPathProvider {
/** /**
* Fetch the learning paths with the given hruids from the data source. * Fetch the learning paths with the given hruids from the data source.
*/ */
fetchLearningPaths(hruids: string[], language: Language, source: string, personalizedFor?: PersonalizationTarget): Promise<LearningPathResponse>; fetchLearningPaths(hruids: string[], language: Language, source: string, personalizedFor?: Group): Promise<LearningPathResponse>;
/** /**
* Search learning paths in the data source using the given search string. * Search learning paths in the data source using the given search string.
*/ */
searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]>; searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]>;
} }

View file

@ -1,13 +1,78 @@
import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js';
import databaseLearningPathProvider from './database-learning-path-provider.js'; import databaseLearningPathProvider from './database-learning-path-provider.js';
import { envVars, getEnvVar } from '../../util/envVars.js'; import { envVars, getEnvVar } from '../../util/envVars.js';
import { PersonalizationTarget } from './learning-path-personalization-util.js'; import { LearningObjectNode, LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { Group } from '../../entities/assignments/group.entity.js';
import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.entity.js';
import { getLearningPathRepository } from '../../data/repositories.js';
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
import { base64ToArrayBuffer } from '../../util/base64-buffer-conversion.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import { mapToTeacher } from '../../interfaces/teacher.js';
import { Collection } from '@mikro-orm/core';
const userContentPrefix = getEnvVar(envVars.UserContentPrefix); const userContentPrefix = getEnvVar(envVars.UserContentPrefix);
const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider]; const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider];
export function mapToLearningPath(dto: LearningPath, adminsDto: TeacherDTO[]): LearningPathEntity {
const admins = adminsDto.map((admin) => mapToTeacher(admin));
const repo = getLearningPathRepository();
const path = repo.create({
hruid: dto.hruid,
language: dto.language as Language,
description: dto.description,
title: dto.title,
admins,
image: dto.image ? Buffer.from(base64ToArrayBuffer(dto.image)) : null,
});
const nodes = dto.nodes.map((nodeDto: LearningObjectNode, i: number) =>
repo.createNode({
learningPath: path,
learningObjectHruid: nodeDto.learningobject_hruid,
nodeNumber: i,
language: nodeDto.language,
version: nodeDto.version,
startNode: nodeDto.start_node ?? false,
createdAt: new Date(),
updatedAt: new Date(),
})
);
dto.nodes.forEach((nodeDto) => {
const fromNode = nodes.find(
(it) => it.learningObjectHruid === nodeDto.learningobject_hruid && it.language === nodeDto.language && it.version === nodeDto.version
)!;
const transitions = nodeDto.transitions
.map((transDto, i) => {
const toNode = nodes.find(
(it) =>
it.learningObjectHruid === transDto.next.hruid &&
it.language === transDto.next.language &&
it.version === transDto.next.version
);
if (toNode) {
return repo.createTransition({
transitionNumber: i,
node: fromNode,
next: toNode,
condition: transDto.condition ?? 'true',
});
}
return undefined;
})
.filter((it) => it)
.map((it) => it!);
fromNode.transitions = new Collection<LearningPathTransition>(fromNode, transitions);
});
path.nodes = new Collection<LearningPathNode>(path, nodes);
return path;
}
/** /**
* Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api)
*/ */
@ -19,12 +84,7 @@ const learningPathService = {
* @param source * @param source
* @param personalizedFor If this is set, a learning path personalized for the given group or student will be returned. * @param personalizedFor If this is set, a learning path personalized for the given group or student will be returned.
*/ */
async fetchLearningPaths( async fetchLearningPaths(hruids: string[], language: Language, source: string, personalizedFor?: Group): Promise<LearningPathResponse> {
hruids: string[],
language: Language,
source: string,
personalizedFor?: PersonalizationTarget
): Promise<LearningPathResponse> {
const userContentHruids = hruids.filter((hruid) => hruid.startsWith(userContentPrefix)); const userContentHruids = hruids.filter((hruid) => hruid.startsWith(userContentPrefix));
const nonUserContentHruids = hruids.filter((hruid) => !hruid.startsWith(userContentPrefix)); const nonUserContentHruids = hruids.filter((hruid) => !hruid.startsWith(userContentPrefix));
@ -48,12 +108,23 @@ const learningPathService = {
/** /**
* Search learning paths in the data source using the given search string. * Search learning paths in the data source using the given search string.
*/ */
async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> { async searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]> {
const providerResponses = await Promise.all( const providerResponses = await Promise.all(
allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor)) allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor))
); );
return providerResponses.flat(); return providerResponses.flat();
}, },
/**
* Add a new learning path to the database.
* @param dto Learning path DTO from which the learning path will be created.
* @param admins Teachers who should become an admin of the learning path.
*/
async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<void> {
const repo = getLearningPathRepository();
const path = mapToLearningPath(dto, admins);
await repo.save(path, { preventOverwrite: true });
},
}; };
export default learningPathService; export default learningPathService;

View file

@ -7,7 +7,7 @@ import {
getSubmissionRepository, getSubmissionRepository,
} from '../data/repositories.js'; } from '../data/repositories.js';
import { mapToClassDTO } from '../interfaces/class.js'; import { mapToClassDTO } from '../interfaces/class.js';
import { mapToGroupDTO, mapToShallowGroupDTO } from '../interfaces/group.js'; import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js'; import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
import { getAllAssignments } from './assignments.js'; import { getAllAssignments } from './assignments.js';
@ -18,8 +18,8 @@ import { NotFoundException } from '../exceptions/not-found-exception.js';
import { fetchClass } from './classes.js'; import { fetchClass } from './classes.js';
import { StudentDTO } from '@dwengo-1/common/interfaces/student'; import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { ClassDTO } from '@dwengo-1/common/interfaces/class'; import { ClassDTO } from '@dwengo-1/common/interfaces/class';
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { AssignmentDTO, AssignmentDTOId } from '@dwengo-1/common/interfaces/assignment';
import { GroupDTO } from '@dwengo-1/common/interfaces/group'; import { GroupDTO, GroupDTOId } from '@dwengo-1/common/interfaces/group';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
@ -48,6 +48,11 @@ export async function fetchStudent(username: string): Promise<Student> {
return user; return user;
} }
export async function fetchStudents(usernames: string[]): Promise<Student[]> {
const members = await Promise.all(usernames.map(async (username) => await fetchStudent(username)));
return members;
}
export async function getStudent(username: string): Promise<StudentDTO> { export async function getStudent(username: string): Promise<StudentDTO> {
const user = await fetchStudent(username); const user = await fetchStudent(username);
return mapToStudentDTO(user); return mapToStudentDTO(user);
@ -83,7 +88,7 @@ export async function getStudentClasses(username: string, full: boolean): Promis
return classes.map((cls) => cls.classId!); return classes.map((cls) => cls.classId!);
} }
export async function getStudentAssignments(username: string, full: boolean): Promise<AssignmentDTO[]> { export async function getStudentAssignments(username: string, full: boolean): Promise<AssignmentDTO[] | AssignmentDTOId[]> {
const student = await fetchStudent(username); const student = await fetchStudent(username);
const classRepository = getClassRepository(); const classRepository = getClassRepository();
@ -92,17 +97,17 @@ export async function getStudentAssignments(username: string, full: boolean): Pr
return (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat(); return (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat();
} }
export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[]> { export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[] | GroupDTOId[]> {
const student = await fetchStudent(username); const student = await fetchStudent(username);
const groupRepository = getGroupRepository(); const groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsWithStudent(student); const groups = await groupRepository.findAllGroupsWithStudent(student);
if (full) { if (full) {
return groups.map(mapToGroupDTO); return groups.map((group) => mapToGroupDTO(group, group.assignment.within));
} }
return groups.map(mapToShallowGroupDTO); return groups.map((group) => mapToGroupDTOId(group, group.assignment.within));
} }
export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> { export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> {

View file

@ -1,4 +1,4 @@
import { getAssignmentRepository, getSubmissionRepository } from '../data/repositories.js'; import { getAssignmentRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { NotFoundException } from '../exceptions/not-found-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js';
import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js'; import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js';
@ -33,10 +33,11 @@ export async function getAllSubmissions(loId: LearningObjectIdentifier): Promise
export async function createSubmission(submissionDTO: SubmissionDTO): Promise<SubmissionDTO> { export async function createSubmission(submissionDTO: SubmissionDTO): Promise<SubmissionDTO> {
const submitter = await fetchStudent(submissionDTO.submitter.username); const submitter = await fetchStudent(submissionDTO.submitter.username);
const group = await getExistingGroupFromGroupDTO(submissionDTO.group); const group = await getExistingGroupFromGroupDTO(submissionDTO.group!);
const submissionRepository = getSubmissionRepository(); const submissionRepository = getSubmissionRepository();
const submission = mapToSubmission(submissionDTO, submitter, group); const submission = mapToSubmission(submissionDTO, submitter, group);
await submissionRepository.save(submission); await submissionRepository.save(submission);
return mapToSubmissionDTO(submission); return mapToSubmissionDTO(submission);
@ -60,12 +61,18 @@ export async function getSubmissionsForLearningObjectAndAssignment(
version: number, version: number,
classId: string, classId: string,
assignmentId: number, assignmentId: number,
studentUsername?: string groupId?: number
): Promise<SubmissionDTO[]> { ): Promise<SubmissionDTO[]> {
const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); 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); let submissions: Submission[];
if (groupId !== undefined) {
const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, groupId);
submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndGroup(loId, group!);
} else {
submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!);
}
return submissions.map((s) => mapToSubmissionDTO(s)); return submissions.map((s) => mapToSubmissionDTO(s));
} }

View file

@ -27,7 +27,7 @@ export class SqliteAutoincrementSubscriber implements EventSubscriber {
for (const prop of Object.values(args.meta.properties)) { for (const prop of Object.values(args.meta.properties)) {
const property = prop as EntityProperty<T>; const property = prop as EntityProperty<T>;
if (property.primary && property.autoincrement && !(args.entity as Record<string, unknown>)[property.name]) { if (property.primary && property.autoincrement && (args.entity as Record<string, unknown>)[property.name] === undefined) {
// Obtain and increment sequence number of this entity. // Obtain and increment sequence number of this entity.
const propertyKey = args.meta.class.name + '.' + property.name; const propertyKey = args.meta.class.name + '.' + property.name;
const nextSeqNumber = this.sequenceNumbersForEntityType.get(propertyKey) || 0; const nextSeqNumber = this.sequenceNumbersForEntityType.get(propertyKey) || 0;

View file

@ -0,0 +1,12 @@
/**
* Convert a Base64-encoded string into a buffer with the same data.
* @param base64 The Base64 encoded string.
*/
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}

View file

@ -16,7 +16,7 @@ describe('AssignmentRepository', () => {
it('should return the requested assignment', async () => { it('should return the requested assignment', async () => {
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89'); const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
const assignment = await assignmentRepository.findByClassAndId(class_!, 2); const assignment = await assignmentRepository.findByClassAndId(class_!, 21001);
expect(assignment).toBeTruthy(); expect(assignment).toBeTruthy();
expect(assignment!.title).toBe('tool'); expect(assignment!.title).toBe('tool');
@ -35,7 +35,7 @@ describe('AssignmentRepository', () => {
const result = await assignmentRepository.findAllByResponsibleTeacher('testleerkracht1'); const result = await assignmentRepository.findAllByResponsibleTeacher('testleerkracht1');
const resultIds = result.map((it) => it.id).sort((a, b) => (a ?? 0) - (b ?? 0)); const resultIds = result.map((it) => it.id).sort((a, b) => (a ?? 0) - (b ?? 0));
expect(resultIds).toEqual([1, 3, 4]); expect(resultIds).toEqual([21000, 21002, 21003, 21004]);
}); });
it('should not find removed assignment', async () => { it('should not find removed assignment', async () => {

View file

@ -19,16 +19,16 @@ describe('GroupRepository', () => {
it('should return the requested group', async () => { it('should return the requested group', async () => {
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
const assignment = await assignmentRepository.findByClassAndId(class_!, 1); const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001);
expect(group).toBeTruthy(); expect(group).toBeTruthy();
}); });
it('should return all groups for assignment', async () => { it('should return all groups for assignment', async () => {
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
const assignment = await assignmentRepository.findByClassAndId(class_!, 1); const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
const groups = await groupRepository.findAllGroupsForAssignment(assignment!); const groups = await groupRepository.findAllGroupsForAssignment(assignment!);
@ -38,9 +38,9 @@ describe('GroupRepository', () => {
it('should not find removed group', async () => { it('should not find removed group', async () => {
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89'); const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
const assignment = await assignmentRepository.findByClassAndId(class_!, 2); const assignment = await assignmentRepository.findByClassAndId(class_!, 21001);
await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, 1); await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, 21001);
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1);

View file

@ -54,8 +54,8 @@ describe('SubmissionRepository', () => {
it('should find the most recent submission for a group', async () => { it('should find the most recent submission for a group', async () => {
const id = new LearningObjectIdentifier('id03', Language.English, 1); const id = new LearningObjectIdentifier('id03', Language.English, 1);
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
const assignment = await assignmentRepository.findByClassAndId(class_!, 1); const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001);
const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, group!); const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, group!);
expect(submission).toBeTruthy(); expect(submission).toBeTruthy();
@ -67,7 +67,7 @@ describe('SubmissionRepository', () => {
let loId: LearningObjectIdentifier; let loId: LearningObjectIdentifier;
it('should find all submissions for a certain learning object and assignment', async () => { it('should find all submissions for a certain learning object and assignment', async () => {
clazz = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); clazz = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
assignment = await assignmentRepository.findByClassAndId(clazz!, 1); assignment = await assignmentRepository.findByClassAndId(clazz!, 21000);
loId = { loId = {
hruid: 'id02', hruid: 'id02',
language: Language.English, language: Language.English,
@ -91,9 +91,9 @@ describe('SubmissionRepository', () => {
expect(result[2].submissionNumber).toBe(3); expect(result[2].submissionNumber).toBe(3);
}); });
it("should find only the submissions for a certain learning object and assignment made for the user's group", async () => { it('should find only the submissions for a certain learning object and assignment made for the given group', async () => {
const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, 'Tool'); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21002);
// (student Tool is in group #2) const result = await submissionRepository.findAllSubmissionsForLearningObjectAndGroup(loId, group!);
expect(result).toHaveLength(1); expect(result).toHaveLength(1);

View file

@ -2,66 +2,57 @@ import { beforeAll, describe, expect, it } from 'vitest';
import { setupTestApp } from '../../setup-tests.js'; import { setupTestApp } from '../../setup-tests.js';
import { getAttachmentRepository, getLearningObjectRepository } from '../../../src/data/repositories.js'; import { getAttachmentRepository, getLearningObjectRepository } from '../../../src/data/repositories.js';
import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js'; import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js';
import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js';
import example from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.js';
import { LearningObject } from '../../../src/entities/content/learning-object.entity.js'; import { LearningObject } from '../../../src/entities/content/learning-object.entity.js';
import { Attachment } from '../../../src/entities/content/attachment.entity.js'; import { Attachment } from '../../../src/entities/content/attachment.entity.js';
import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier.js';
import { testLearningObjectPnNotebooks } from '../../test_assets/content/learning-objects.testdata';
const NEWER_TEST_SUFFIX = 'nEweR'; import { v4 as uuidV4 } from 'uuid';
async function createTestLearningObjects(learningObjectRepo: LearningObjectRepository): Promise<{
older: LearningObject;
newer: LearningObject;
}> {
const olderExample = example.createLearningObject();
await learningObjectRepo.save(olderExample);
const newerExample = example.createLearningObject();
newerExample.title = 'Newer example';
newerExample.version = 100;
return {
older: olderExample,
newer: newerExample,
};
}
describe('AttachmentRepository', () => { describe('AttachmentRepository', () => {
let attachmentRepo: AttachmentRepository; let attachmentRepo: AttachmentRepository;
let exampleLearningObjects: { older: LearningObject; newer: LearningObject }; let newLearningObject: LearningObject;
let attachmentsOlderLearningObject: Attachment[]; let attachmentsOlderLearningObject: Attachment[];
beforeAll(async () => { beforeAll(async () => {
await setupTestApp(); await setupTestApp();
attachmentsOlderLearningObject = testLearningObjectPnNotebooks.attachments as Attachment[];
attachmentRepo = getAttachmentRepository(); attachmentRepo = getAttachmentRepository();
exampleLearningObjects = await createTestLearningObjects(getLearningObjectRepository()); const learningObjectRepo = getLearningObjectRepository();
});
it('can add attachments to learning objects without throwing an error', async () => { const newLearningObjectData = structuredClone(testLearningObjectPnNotebooks);
attachmentsOlderLearningObject = Object.values(example.createAttachment).map((fn) => fn(exampleLearningObjects.older)); newLearningObjectData.title = 'Newer example';
newLearningObjectData.version = 101;
newLearningObjectData.attachments = [];
newLearningObjectData.uuid = uuidV4();
newLearningObjectData.content = Buffer.from('Content of the newer example');
await Promise.all(attachmentsOlderLearningObject.map(async (attachment) => attachmentRepo.save(attachment))); newLearningObject = learningObjectRepo.create(newLearningObjectData);
await learningObjectRepo.save(newLearningObject);
}); });
let attachmentOnlyNewer: Attachment; let attachmentOnlyNewer: Attachment;
it('allows us to add attachments with the same name to a different learning object without throwing an error', async () => { it('allows us to add attachments with the same name to a different learning object without throwing an error', async () => {
attachmentOnlyNewer = Object.values(example.createAttachment)[0](exampleLearningObjects.newer); attachmentOnlyNewer = structuredClone(attachmentsOlderLearningObject[0]);
attachmentOnlyNewer.content.write(NEWER_TEST_SUFFIX); attachmentOnlyNewer.learningObject = newLearningObject;
attachmentOnlyNewer.content = Buffer.from('New attachment content');
await attachmentRepo.save(attachmentOnlyNewer); await attachmentRepo.save(attachmentRepo.create(attachmentOnlyNewer));
}); });
let olderLearningObjectId: LearningObjectIdentifier; let olderLearningObjectId: LearningObjectIdentifier;
it('returns the correct attachment when queried by learningObjectId and attachment name', async () => { it('returns the correct attachment when queried by learningObjectId and attachment name', async () => {
olderLearningObjectId = { olderLearningObjectId = {
hruid: exampleLearningObjects.older.hruid, hruid: testLearningObjectPnNotebooks.hruid,
language: exampleLearningObjects.older.language, language: testLearningObjectPnNotebooks.language,
version: exampleLearningObjects.older.version, version: testLearningObjectPnNotebooks.version,
}; };
const result = await attachmentRepo.findByLearningObjectIdAndName(olderLearningObjectId, attachmentsOlderLearningObject[0].name); const result = await attachmentRepo.findByLearningObjectIdAndName(olderLearningObjectId, attachmentsOlderLearningObject[0].name);
expect(result).toBe(attachmentsOlderLearningObject[0]); expect(result).not.toBeNull();
expect(result!.name).toEqual(attachmentsOlderLearningObject[0].name);
expect(result!.content).toEqual(attachmentsOlderLearningObject[0].content);
}); });
it('returns null when queried by learningObjectId and non-existing attachment name', async () => { it('returns null when queried by learningObjectId and non-existing attachment name', async () => {
@ -71,10 +62,12 @@ describe('AttachmentRepository', () => {
it('returns the newer version of the attachment when only queried by hruid, language and attachment name (but not version)', async () => { it('returns the newer version of the attachment when only queried by hruid, language and attachment name (but not version)', async () => {
const result = await attachmentRepo.findByMostRecentVersionOfLearningObjectAndName( const result = await attachmentRepo.findByMostRecentVersionOfLearningObjectAndName(
exampleLearningObjects.older.hruid, testLearningObjectPnNotebooks.hruid,
exampleLearningObjects.older.language, testLearningObjectPnNotebooks.language,
attachmentOnlyNewer.name attachmentOnlyNewer.name
); );
expect(result).toBe(attachmentOnlyNewer); expect(result).not.toBeNull();
expect(result!.name).toEqual(attachmentOnlyNewer.name);
expect(result!.content).toEqual(attachmentOnlyNewer.content);
}); });
}); });

View file

@ -1,28 +1,21 @@
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
import { setupTestApp } from '../../setup-tests.js'; import { setupTestApp } from '../../setup-tests.js';
import { getAttachmentRepository, getLearningObjectRepository } from '../../../src/data/repositories.js'; import { getAttachmentRepository } from '../../../src/data/repositories.js';
import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js'; import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js';
import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js'; import { testLearningObject02 } from '../../test_assets/content/learning-objects.testdata';
import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier.js';
import { Language } from '@dwengo-1/common/util/language';
describe('AttachmentRepository', () => { describe('AttachmentRepository', () => {
let attachmentRepository: AttachmentRepository; let attachmentRepository: AttachmentRepository;
let learningObjectRepository: LearningObjectRepository;
beforeAll(async () => { beforeAll(async () => {
await setupTestApp(); await setupTestApp();
attachmentRepository = getAttachmentRepository(); attachmentRepository = getAttachmentRepository();
learningObjectRepository = getLearningObjectRepository();
}); });
it('should return the requested attachment', async () => { it('should return the requested attachment', async () => {
const id = new LearningObjectIdentifier('id02', Language.English, 1);
const learningObject = await learningObjectRepository.findByIdentifier(id);
const attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName( const attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName(
learningObject!.hruid, testLearningObject02.hruid,
Language.English, testLearningObject02.language,
'attachment01' 'attachment01'
); );

View file

@ -2,48 +2,33 @@ import { beforeAll, describe, it, expect } from 'vitest';
import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js'; import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js';
import { setupTestApp } from '../../setup-tests.js'; import { setupTestApp } from '../../setup-tests.js';
import { getLearningObjectRepository } from '../../../src/data/repositories.js'; import { getLearningObjectRepository } from '../../../src/data/repositories.js';
import example from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.js';
import { LearningObject } from '../../../src/entities/content/learning-object.entity.js'; import { LearningObject } from '../../../src/entities/content/learning-object.entity.js';
import { expectToBeCorrectEntity } from '../../test-utils/expectations.js'; import { expectToBeCorrectEntity } from '../../test-utils/expectations.js';
import { testLearningObject01, testLearningObject02, testLearningObject03 } from '../../test_assets/content/learning-objects.testdata';
import { v4 } from 'uuid';
describe('LearningObjectRepository', () => { describe('LearningObjectRepository', () => {
let learningObjectRepository: LearningObjectRepository; let learningObjectRepository: LearningObjectRepository;
let exampleLearningObject: LearningObject;
beforeAll(async () => { beforeAll(async () => {
await setupTestApp(); await setupTestApp();
learningObjectRepository = getLearningObjectRepository(); learningObjectRepository = getLearningObjectRepository();
}); });
it('should be able to add a learning object to it without an error', async () => { it('should return a learning object when queried by id', async () => {
exampleLearningObject = example.createLearningObject();
await learningObjectRepository.insert(exampleLearningObject);
});
it('should return the learning object when queried by id', async () => {
const result = await learningObjectRepository.findByIdentifier({ const result = await learningObjectRepository.findByIdentifier({
hruid: exampleLearningObject.hruid, hruid: testLearningObject01.hruid,
language: exampleLearningObject.language, language: testLearningObject02.language,
version: exampleLearningObject.version, version: testLearningObject03.version,
}); });
expect(result).toBeInstanceOf(LearningObject); expect(result).toBeInstanceOf(LearningObject);
expectToBeCorrectEntity( expectToBeCorrectEntity(result!, testLearningObject01);
{
name: 'actual',
entity: result!,
},
{
name: 'expected',
entity: exampleLearningObject,
}
);
}); });
it('should return null when non-existing version is queried', async () => { it('should return null when non-existing version is queried', async () => {
const result = await learningObjectRepository.findByIdentifier({ const result = await learningObjectRepository.findByIdentifier({
hruid: exampleLearningObject.hruid, hruid: testLearningObject01.hruid,
language: exampleLearningObject.language, language: testLearningObject01.language,
version: 100, version: 100,
}); });
expect(result).toBe(null); expect(result).toBe(null);
@ -52,9 +37,12 @@ describe('LearningObjectRepository', () => {
let newerExample: LearningObject; let newerExample: LearningObject;
it('should allow a learning object with the same id except a different version to be added', async () => { it('should allow a learning object with the same id except a different version to be added', async () => {
newerExample = example.createLearningObject(); const testLearningObject01Newer = structuredClone(testLearningObject01);
newerExample.version = 10; testLearningObject01Newer.version = 10;
newerExample.title += ' (nieuw)'; testLearningObject01Newer.title += ' (nieuw)';
testLearningObject01Newer.uuid = v4();
testLearningObject01Newer.content = Buffer.from('This is the new content.');
newerExample = learningObjectRepository.create(testLearningObject01Newer);
await learningObjectRepository.save(newerExample); await learningObjectRepository.save(newerExample);
}); });
@ -66,7 +54,7 @@ describe('LearningObjectRepository', () => {
}); });
it('should return null when queried by non-existing hruid or language', async () => { it('should return null when queried by non-existing hruid or language', async () => {
const result = await learningObjectRepository.findLatestByHruidAndLanguage('something_that_does_not_exist', exampleLearningObject.language); const result = await learningObjectRepository.findLatestByHruidAndLanguage('something_that_does_not_exist', testLearningObject01.language);
expect(result).toBe(null); expect(result).toBe(null);
}); });
}); });

View file

@ -4,6 +4,7 @@ import { getLearningObjectRepository } from '../../../src/data/repositories';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata';
describe('LearningObjectRepository', () => { describe('LearningObjectRepository', () => {
let learningObjectRepository: LearningObjectRepository; let learningObjectRepository: LearningObjectRepository;
@ -13,8 +14,8 @@ describe('LearningObjectRepository', () => {
learningObjectRepository = getLearningObjectRepository(); learningObjectRepository = getLearningObjectRepository();
}); });
const id01 = new LearningObjectIdentifier('id01', Language.English, 1); const id01 = new LearningObjectIdentifier(testLearningObject01.hruid, testLearningObject01.language, testLearningObject01.version);
const id02 = new LearningObjectIdentifier('test_id', Language.English, 1); const id02 = new LearningObjectIdentifier('non_existing_id', Language.English, 1);
it('should return the learning object that matches identifier 1', async () => { it('should return the learning object that matches identifier 1', async () => {
const learningObject = await learningObjectRepository.findByIdentifier(id01); const learningObject = await learningObjectRepository.findByIdentifier(id01);

View file

@ -2,41 +2,27 @@ import { beforeAll, describe, expect, it } from 'vitest';
import { setupTestApp } from '../../setup-tests.js'; import { setupTestApp } from '../../setup-tests.js';
import { getLearningPathRepository } from '../../../src/data/repositories.js'; import { getLearningPathRepository } from '../../../src/data/repositories.js';
import { LearningPathRepository } from '../../../src/data/content/learning-path-repository.js'; import { LearningPathRepository } from '../../../src/data/content/learning-path-repository.js';
import example from '../../test-assets/learning-paths/pn-werking-example.js';
import { LearningPath } from '../../../src/entities/content/learning-path.entity.js'; import { LearningPath } from '../../../src/entities/content/learning-path.entity.js';
import { expectToBeCorrectEntity } from '../../test-utils/expectations.js'; import { expectToBeCorrectEntity, expectToHaveFoundNothing, expectToHaveFoundPrecisely } from '../../test-utils/expectations.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { testLearningPath01 } from '../../test_assets/content/learning-paths.testdata';
function expectToHaveFoundPrecisely(expected: LearningPath, result: LearningPath[]): void { import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service';
expect(result).toHaveProperty('length');
expect(result.length).toBe(1);
expectToBeCorrectEntity({ entity: result[0] }, { entity: expected });
}
function expectToHaveFoundNothing(result: LearningPath[]): void {
expect(result).toHaveProperty('length');
expect(result.length).toBe(0);
}
describe('LearningPathRepository', () => { describe('LearningPathRepository', () => {
let learningPathRepo: LearningPathRepository; let learningPathRepo: LearningPathRepository;
let examplePath: LearningPath;
beforeAll(async () => { beforeAll(async () => {
await setupTestApp(); await setupTestApp();
learningPathRepo = getLearningPathRepository(); learningPathRepo = getLearningPathRepository();
examplePath = mapToLearningPath(testLearningPath01, []);
}); });
let examplePath: LearningPath; it('should return a learning path when it is queried by hruid and language', async () => {
const result = await learningPathRepo.findByHruidAndLanguage(testLearningPath01.hruid, testLearningPath01.language as Language);
it('should be able to add a learning path without throwing an error', async () => {
examplePath = example.createLearningPath();
await learningPathRepo.insert(examplePath);
});
it('should return the added path when it is queried by hruid and language', async () => {
const result = await learningPathRepo.findByHruidAndLanguage(examplePath.hruid, examplePath.language);
expect(result).toBeInstanceOf(LearningPath); expect(result).toBeInstanceOf(LearningPath);
expectToBeCorrectEntity({ entity: result! }, { entity: examplePath }); expectToBeCorrectEntity(result!, examplePath);
}); });
it('should return null to a query on a non-existing hruid or language', async () => { it('should return null to a query on a non-existing hruid or language', async () => {
@ -45,7 +31,7 @@ describe('LearningPathRepository', () => {
}); });
it('should return the learning path when we search for a search term occurring in its title', async () => { it('should return the learning path when we search for a search term occurring in its title', async () => {
const result = await learningPathRepo.findByQueryStringAndLanguage(examplePath.title.slice(4, 9), examplePath.language); const result = await learningPathRepo.findByQueryStringAndLanguage(examplePath.title.slice(9, 13), examplePath.language);
expectToHaveFoundPrecisely(examplePath, result); expectToHaveFoundPrecisely(examplePath, result);
}); });

View file

@ -3,6 +3,7 @@ import { getLearningPathRepository } from '../../../src/data/repositories';
import { LearningPathRepository } from '../../../src/data/content/learning-path-repository'; import { LearningPathRepository } from '../../../src/data/content/learning-path-repository';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { testLearningPath01 } from '../../test_assets/content/learning-paths.testdata';
describe('LearningPathRepository', () => { describe('LearningPathRepository', () => {
let learningPathRepository: LearningPathRepository; let learningPathRepository: LearningPathRepository;
@ -19,10 +20,10 @@ describe('LearningPathRepository', () => {
}); });
it('should return requested learning path', async () => { it('should return requested learning path', async () => {
const learningPath = await learningPathRepository.findByHruidAndLanguage('id01', Language.English); const learningPath = await learningPathRepository.findByHruidAndLanguage(testLearningPath01.hruid, testLearningPath01.language as Language);
expect(learningPath).toBeTruthy(); expect(learningPath).toBeTruthy();
expect(learningPath?.title).toBe('repertoire Tool'); expect(learningPath?.title).toBe(testLearningPath01.title);
expect(learningPath?.description).toBe('all about Tool'); expect(learningPath?.description).toBe(testLearningPath01.description);
}); });
}); });

View file

@ -38,8 +38,8 @@ describe('QuestionRepository', () => {
const student = await studentRepository.findByUsername('Noordkaap'); const student = await studentRepository.findByUsername('Noordkaap');
const clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 1); const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000);
const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 1); const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 21001);
await questionRepository.createQuestion({ await questionRepository.createQuestion({
loId: id, loId: id,
inGroup: group!, inGroup: group!,
@ -57,7 +57,7 @@ describe('QuestionRepository', () => {
let loId: LearningObjectIdentifier; let loId: LearningObjectIdentifier;
it('should find all questions for a certain learning object and assignment', async () => { it('should find all questions for a certain learning object and assignment', async () => {
clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
assignment = await getAssignmentRepository().findByClassAndId(clazz!, 1); assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000);
loId = { loId = {
hruid: 'id05', hruid: 'id05',
language: Language.English, language: Language.English,

View file

@ -1,37 +1,32 @@
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories';
import example from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example';
import { LearningObject } from '../../../src/entities/content/learning-object.entity'; import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import databaseLearningObjectProvider from '../../../src/services/learning-objects/database-learning-object-provider'; import databaseLearningObjectProvider from '../../../src/services/learning-objects/database-learning-object-provider';
import { expectToBeCorrectFilteredLearningObject } from '../../test-utils/expectations'; import { expectToBeCorrectFilteredLearningObject } from '../../test-utils/expectations';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example'; import { FilteredLearningObject, LearningObjectNode, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; import { testPartiallyDatabaseAndPartiallyDwengoApiLearningPath } from '../../test_assets/content/learning-paths.testdata';
import { LearningPath } from '../../../src/entities/content/learning-path.entity'; import { testLearningObjectPnNotebooks } from '../../test_assets/content/learning-objects.testdata';
import { FilteredLearningObject } from '@dwengo-1/common/interfaces/learning-content'; import { LearningPath } from '@dwengo-1/common/dist/interfaces/learning-content';
import { RequiredEntityData } from '@mikro-orm/core';
async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { import { getHtmlRenderingForTestLearningObject } from '../../test-utils/get-html-rendering';
const learningObjectRepo = getLearningObjectRepository();
const learningPathRepo = getLearningPathRepository();
const learningObject = learningObjectExample.createLearningObject();
const learningPath = learningPathExample.createLearningPath();
await learningObjectRepo.save(learningObject);
await learningPathRepo.save(learningPath);
return { learningObject, learningPath };
}
const EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT = 'Notebook opslaan'; const EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT = 'Notebook opslaan';
describe('DatabaseLearningObjectProvider', () => { describe('DatabaseLearningObjectProvider', () => {
let exampleLearningObject: LearningObject; let exampleLearningObject: RequiredEntityData<LearningObject>;
let exampleLearningPath: LearningPath; let exampleLearningPath: LearningPath;
let exampleLearningPathId: LearningPathIdentifier;
beforeAll(async () => { beforeAll(async () => {
await setupTestApp(); await setupTestApp();
const exampleData = await initExampleData(); exampleLearningObject = testLearningObjectPnNotebooks;
exampleLearningObject = exampleData.learningObject; exampleLearningPath = testPartiallyDatabaseAndPartiallyDwengoApiLearningPath;
exampleLearningPath = exampleData.learningPath;
exampleLearningPathId = {
hruid: exampleLearningPath.hruid,
language: exampleLearningPath.language as Language,
};
}); });
describe('getLearningObjectById', () => { describe('getLearningObjectById', () => {
it('should return the learning object when it is queried by its id', async () => { it('should return the learning object when it is queried by its id', async () => {
@ -61,7 +56,7 @@ describe('DatabaseLearningObjectProvider', () => {
it('should return the correct rendering of the learning object', async () => { it('should return the correct rendering of the learning object', async () => {
const result = await databaseLearningObjectProvider.getLearningObjectHTML(exampleLearningObject); const result = await databaseLearningObjectProvider.getLearningObjectHTML(exampleLearningObject);
// Set newlines so your tests are platform-independent. // Set newlines so your tests are platform-independent.
expect(result).toEqual(example.getHTMLRendering().replace(/\r\n/g, '\n')); expect(result).toEqual(getHtmlRenderingForTestLearningObject(exampleLearningObject).replace(/\r\n/g, '\n'));
}); });
it('should return null for a non-existing learning object', async () => { it('should return null for a non-existing learning object', async () => {
const result = await databaseLearningObjectProvider.getLearningObjectHTML({ const result = await databaseLearningObjectProvider.getLearningObjectHTML({
@ -73,8 +68,8 @@ describe('DatabaseLearningObjectProvider', () => {
}); });
describe('getLearningObjectIdsFromPath', () => { describe('getLearningObjectIdsFromPath', () => {
it('should return all learning object IDs from a path', async () => { it('should return all learning object IDs from a path', async () => {
const result = await databaseLearningObjectProvider.getLearningObjectIdsFromPath(exampleLearningPath); const result = await databaseLearningObjectProvider.getLearningObjectIdsFromPath(exampleLearningPathId);
expect(new Set(result)).toEqual(new Set(exampleLearningPath.nodes.map((it) => it.learningObjectHruid))); expect(new Set(result)).toEqual(new Set(exampleLearningPath.nodes.map((it: LearningObjectNode) => it.learningobject_hruid)));
}); });
it('should throw an error if queried with a path identifier for which there is no learning path', async () => { it('should throw an error if queried with a path identifier for which there is no learning path', async () => {
await expect( await expect(
@ -89,9 +84,11 @@ describe('DatabaseLearningObjectProvider', () => {
}); });
describe('getLearningObjectsFromPath', () => { describe('getLearningObjectsFromPath', () => {
it('should correctly return all learning objects which are on the path, even those who are not in the database', async () => { it('should correctly return all learning objects which are on the path, even those who are not in the database', async () => {
const result = await databaseLearningObjectProvider.getLearningObjectsFromPath(exampleLearningPath); const result = await databaseLearningObjectProvider.getLearningObjectsFromPath(exampleLearningPathId);
expect(result.length).toBe(exampleLearningPath.nodes.length); expect(result.length).toBe(exampleLearningPath.nodes.length);
expect(new Set(result.map((it) => it.key))).toEqual(new Set(exampleLearningPath.nodes.map((it) => it.learningObjectHruid))); expect(new Set(result.map((it) => it.key))).toEqual(
new Set(exampleLearningPath.nodes.map((it: LearningObjectNode) => it.learningobject_hruid))
);
expect(result.map((it) => it.title)).toContainEqual(EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT); expect(result.map((it) => it.title)).toContainEqual(EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT);
}); });

View file

@ -1,14 +1,14 @@
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import { LearningObject } from '../../../src/entities/content/learning-object.entity'; import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories';
import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example';
import learningObjectService from '../../../src/services/learning-objects/learning-object-service'; import learningObjectService from '../../../src/services/learning-objects/learning-object-service';
import { envVars, getEnvVar } from '../../../src/util/envVars'; import { envVars, getEnvVar } from '../../../src/util/envVars';
import { LearningPath } from '../../../src/entities/content/learning-path.entity'; import { LearningObjectIdentifierDTO, LearningPath as LearningPathDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import learningPathExample from '../../test-assets/learning-paths/pn-werking-example';
import { LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { testLearningObjectPnNotebooks } from '../../test_assets/content/learning-objects.testdata';
import { testPartiallyDatabaseAndPartiallyDwengoApiLearningPath } from '../../test_assets/content/learning-paths.testdata';
import { RequiredEntityData } from '@mikro-orm/core';
import { getHtmlRenderingForTestLearningObject } from '../../test-utils/get-html-rendering';
const EXPECTED_DWENGO_LEARNING_OBJECT_TITLE = 'Werken met notebooks'; const EXPECTED_DWENGO_LEARNING_OBJECT_TITLE = 'Werken met notebooks';
const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifierDTO = { const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifierDTO = {
@ -23,25 +23,20 @@ const DWENGO_TEST_LEARNING_PATH_ID: LearningPathIdentifier = {
}; };
const DWENGO_TEST_LEARNING_PATH_HRUIDS = new Set(['pn_werkingnotebooks', 'pn_werkingnotebooks2', 'pn_werkingnotebooks3']); const DWENGO_TEST_LEARNING_PATH_HRUIDS = new Set(['pn_werkingnotebooks', 'pn_werkingnotebooks2', 'pn_werkingnotebooks3']);
async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> {
const learningObjectRepo = getLearningObjectRepository();
const learningPathRepo = getLearningPathRepository();
const learningObject = learningObjectExample.createLearningObject();
const learningPath = learningPathExample.createLearningPath();
await learningObjectRepo.save(learningObject);
await learningPathRepo.save(learningPath);
return { learningObject, learningPath };
}
describe('LearningObjectService', () => { describe('LearningObjectService', () => {
let exampleLearningObject: LearningObject; let exampleLearningObject: RequiredEntityData<LearningObject>;
let exampleLearningPath: LearningPath; let exampleLearningPath: LearningPathDTO;
let exampleLearningPathId: LearningPathIdentifier;
beforeAll(async () => { beforeAll(async () => {
await setupTestApp(); await setupTestApp();
const exampleData = await initExampleData(); exampleLearningObject = testLearningObjectPnNotebooks;
exampleLearningObject = exampleData.learningObject; exampleLearningPath = testPartiallyDatabaseAndPartiallyDwengoApiLearningPath;
exampleLearningPath = exampleData.learningPath;
exampleLearningPathId = {
hruid: exampleLearningPath.hruid,
language: exampleLearningPath.language as Language,
};
}); });
describe('getLearningObjectById', () => { describe('getLearningObjectById', () => {
@ -69,7 +64,7 @@ describe('LearningObjectService', () => {
const result = await learningObjectService.getLearningObjectHTML(exampleLearningObject); const result = await learningObjectService.getLearningObjectHTML(exampleLearningObject);
expect(result).not.toBeNull(); expect(result).not.toBeNull();
// Set newlines so your tests are platform-independent. // Set newlines so your tests are platform-independent.
expect(result).toEqual(learningObjectExample.getHTMLRendering().replace(/\r\n/g, '\n')); expect(result).toEqual(getHtmlRenderingForTestLearningObject(exampleLearningObject).replace(/\r\n/g, '\n'));
}); });
it( it(
'returns the same HTML as the Dwengo API when queried with the identifier of a learning object that does ' + 'returns the same HTML as the Dwengo API when queried with the identifier of a learning object that does ' +
@ -97,8 +92,8 @@ describe('LearningObjectService', () => {
describe('getLearningObjectsFromPath', () => { describe('getLearningObjectsFromPath', () => {
it('returns all learning objects when a learning path in the database is queried', async () => { it('returns all learning objects when a learning path in the database is queried', async () => {
const result = await learningObjectService.getLearningObjectsFromPath(exampleLearningPath); const result = await learningObjectService.getLearningObjectsFromPath(exampleLearningPathId);
expect(result.map((it) => it.key)).toEqual(exampleLearningPath.nodes.map((it) => it.learningObjectHruid)); expect(result.map((it) => it.key)).toEqual(exampleLearningPath.nodes.map((it) => it.learningobject_hruid));
}); });
it('also returns all learning objects when a learning path from the Dwengo API is queried', async () => { it('also returns all learning objects when a learning path from the Dwengo API is queried', async () => {
const result = await learningObjectService.getLearningObjectsFromPath(DWENGO_TEST_LEARNING_PATH_ID); const result = await learningObjectService.getLearningObjectsFromPath(DWENGO_TEST_LEARNING_PATH_ID);
@ -115,8 +110,8 @@ describe('LearningObjectService', () => {
describe('getLearningObjectIdsFromPath', () => { describe('getLearningObjectIdsFromPath', () => {
it('returns all learning objects when a learning path in the database is queried', async () => { it('returns all learning objects when a learning path in the database is queried', async () => {
const result = await learningObjectService.getLearningObjectIdsFromPath(exampleLearningPath); const result = await learningObjectService.getLearningObjectIdsFromPath(exampleLearningPathId);
expect(result).toEqual(exampleLearningPath.nodes.map((it) => it.learningObjectHruid)); expect(result).toEqual(exampleLearningPath.nodes.map((it) => it.learningobject_hruid));
}); });
it('also returns all learning object hruids when a learning path from the Dwengo API is queried', async () => { it('also returns all learning object hruids when a learning path from the Dwengo API is queried', async () => {
const result = await learningObjectService.getLearningObjectIdsFromPath(DWENGO_TEST_LEARNING_PATH_ID); const result = await learningObjectService.getLearningObjectIdsFromPath(DWENGO_TEST_LEARNING_PATH_ID);

View file

@ -1,26 +1,35 @@
import { describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
import mdExample from '../../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example';
import multipleChoiceExample from '../../../test-assets/learning-objects/test-multiple-choice/test-multiple-choice-example';
import essayExample from '../../../test-assets/learning-objects/test-essay/test-essay-example';
import processingService from '../../../../src/services/learning-objects/processing/processing-service'; import processingService from '../../../../src/services/learning-objects/processing/processing-service';
import {
testLearningObjectEssayQuestion,
testLearningObjectMultipleChoice,
testLearningObjectPnNotebooks,
} from '../../../test_assets/content/learning-objects.testdata';
import { getHtmlRenderingForTestLearningObject } from '../../../test-utils/get-html-rendering';
import { getLearningObjectRepository } from '../../../../src/data/repositories';
import { setupTestApp } from '../../../setup-tests';
describe('ProcessingService', () => { describe('ProcessingService', () => {
beforeAll(async () => {
await setupTestApp();
});
it('renders a markdown learning object correctly', async () => { it('renders a markdown learning object correctly', async () => {
const markdownLearningObject = mdExample.createLearningObject(); const markdownLearningObject = getLearningObjectRepository().create(testLearningObjectPnNotebooks);
const result = await processingService.render(markdownLearningObject); const result = await processingService.render(markdownLearningObject);
// Set newlines so your tests are platform-independent. // Set newlines so your tests are platform-independent.
expect(result).toEqual(mdExample.getHTMLRendering().replace(/\r\n/g, '\n')); expect(result).toEqual(getHtmlRenderingForTestLearningObject(markdownLearningObject).replace(/\r\n/g, '\n'));
}); });
it('renders a multiple choice question correctly', async () => { it('renders a multiple choice question correctly', async () => {
const multipleChoiceLearningObject = multipleChoiceExample.createLearningObject(); const testLearningObject = getLearningObjectRepository().create(testLearningObjectMultipleChoice);
const result = await processingService.render(multipleChoiceLearningObject); const result = await processingService.render(testLearningObject);
expect(result).toEqual(multipleChoiceExample.getHTMLRendering().replace(/\r\n/g, '\n')); expect(result).toEqual(getHtmlRenderingForTestLearningObject(testLearningObjectMultipleChoice).replace(/\r\n/g, '\n'));
}); });
it('renders an essay question correctly', async () => { it('renders an essay question correctly', async () => {
const essayLearningObject = essayExample.createLearningObject(); const essayLearningObject = getLearningObjectRepository().create(testLearningObjectEssayQuestion);
const result = await processingService.render(essayLearningObject); const result = await processingService.render(essayLearningObject);
expect(result).toEqual(essayExample.getHTMLRendering().replace(/\r\n/g, '\n')); expect(result).toEqual(getHtmlRenderingForTestLearningObject(essayLearningObject).replace(/\r\n/g, '\n'));
}); });
}); });

View file

@ -2,227 +2,112 @@ import { beforeAll, describe, expect, it } from 'vitest';
import { LearningObject } from '../../../src/entities/content/learning-object.entity.js'; import { LearningObject } from '../../../src/entities/content/learning-object.entity.js';
import { setupTestApp } from '../../setup-tests.js'; import { setupTestApp } from '../../setup-tests.js';
import { LearningPath } from '../../../src/entities/content/learning-path.entity.js'; import { LearningPath } from '../../../src/entities/content/learning-path.entity.js';
import { import { getSubmissionRepository } from '../../../src/data/repositories.js';
getAssignmentRepository,
getClassRepository,
getGroupRepository,
getLearningObjectRepository,
getLearningPathRepository,
getStudentRepository,
getSubmissionRepository,
} from '../../../src/data/repositories.js';
import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.js';
import learningPathExample from '../../test-assets/learning-paths/pn-werking-example.js';
import databaseLearningPathProvider from '../../../src/services/learning-paths/database-learning-path-provider.js'; import databaseLearningPathProvider from '../../../src/services/learning-paths/database-learning-path-provider.js';
import { expectToBeCorrectLearningPath } from '../../test-utils/expectations.js'; import { expectToBeCorrectLearningPath } from '../../test-utils/expectations.js';
import learningObjectService from '../../../src/services/learning-objects/learning-object-service.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {
ConditionTestLearningPathAndLearningObjects,
createConditionTestLearningPathAndLearningObjects,
} from '../../test-assets/learning-paths/test-conditions-example.js';
import { Student } from '../../../src/entities/users/student.entity.js';
import { LearningObjectNode, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectNode, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import {
testLearningObject01,
testLearningObjectEssayQuestion,
testLearningObjectMultipleChoice,
} from '../../test_assets/content/learning-objects.testdata';
import { testLearningPathWithConditions } from '../../test_assets/content/learning-paths.testdata';
import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service';
import { getTestGroup01, getTestGroup02 } from '../../test_assets/assignments/groups.testdata';
import { Group } from '../../../src/entities/assignments/group.entity.js';
import { RequiredEntityData } from '@mikro-orm/core';
const STUDENT_A_USERNAME = 'student_a'; function expectBranchingObjectNode(result: LearningPathResponse): LearningObjectNode {
const STUDENT_B_USERNAME = 'student_b'; const branchingObjectMatches = result.data![0].nodes.filter((it) => it.learningobject_hruid === testLearningObjectMultipleChoice.hruid);
const CLASS_NAME = 'test_class';
async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> {
const learningObjectRepo = getLearningObjectRepository();
const learningPathRepo = getLearningPathRepository();
const learningObject = learningObjectExample.createLearningObject();
const learningPath = learningPathExample.createLearningPath();
await learningObjectRepo.save(learningObject);
await learningPathRepo.save(learningPath);
return { learningObject, learningPath };
}
async function initPersonalizationTestData(): Promise<{
learningContent: ConditionTestLearningPathAndLearningObjects;
studentA: Student;
studentB: Student;
}> {
const studentRepo = getStudentRepository();
const classRepo = getClassRepository();
const assignmentRepo = getAssignmentRepository();
const groupRepo = getGroupRepository();
const submissionRepo = getSubmissionRepository();
const learningPathRepo = getLearningPathRepository();
const learningObjectRepo = getLearningObjectRepository();
const learningContent = createConditionTestLearningPathAndLearningObjects();
await learningObjectRepo.save(learningContent.branchingObject);
await learningObjectRepo.save(learningContent.finalObject);
await learningObjectRepo.save(learningContent.extraExerciseObject);
await learningPathRepo.save(learningContent.learningPath);
// Create students
const studentA = studentRepo.create({
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 submissionB = submissionRepo.create({
learningObjectHruid: learningContent.branchingObject.hruid,
learningObjectLanguage: learningContent.branchingObject.language,
learningObjectVersion: learningContent.branchingObject.version,
onBehalfOf: groupA,
submitter: studentB,
submissionTime: new Date(),
content: '[1]',
});
await submissionRepo.save(submissionB);
return {
learningContent: learningContent,
studentA: studentA,
studentB: studentB,
};
}
function expectBranchingObjectNode(
result: LearningPathResponse,
persTestData: {
learningContent: ConditionTestLearningPathAndLearningObjects;
studentA: Student;
studentB: Student;
}
): LearningObjectNode {
const branchingObjectMatches = result.data![0].nodes.filter(
(it) => it.learningobject_hruid === persTestData.learningContent.branchingObject.hruid
);
expect(branchingObjectMatches.length).toBe(1); expect(branchingObjectMatches.length).toBe(1);
return branchingObjectMatches[0]; return branchingObjectMatches[0];
} }
describe('DatabaseLearningPathProvider', () => { describe('DatabaseLearningPathProvider', () => {
let example: { learningObject: LearningObject; learningPath: LearningPath }; let testLearningPath: LearningPath;
let persTestData: { learningContent: ConditionTestLearningPathAndLearningObjects; studentA: Student; studentB: Student }; let branchingLearningObject: RequiredEntityData<LearningObject>;
let extraExerciseLearningObject: RequiredEntityData<LearningObject>;
let finalLearningObject: RequiredEntityData<LearningObject>;
let groupA: Group;
let groupB: Group;
beforeAll(async () => { beforeAll(async () => {
await setupTestApp(); await setupTestApp();
example = await initExampleData(); testLearningPath = mapToLearningPath(testLearningPathWithConditions, []);
persTestData = await initPersonalizationTestData(); branchingLearningObject = testLearningObjectMultipleChoice;
extraExerciseLearningObject = testLearningObject01;
finalLearningObject = testLearningObjectEssayQuestion;
groupA = getTestGroup01();
groupB = getTestGroup02();
// Place different submissions for group A and B.
const submissionRepo = getSubmissionRepository();
const submissionA = submissionRepo.create({
learningObjectHruid: branchingLearningObject.hruid,
learningObjectLanguage: branchingLearningObject.language,
learningObjectVersion: branchingLearningObject.version,
content: '[0]',
onBehalfOf: groupA,
submissionTime: new Date(),
submitter: groupA.members[0],
});
await submissionRepo.save(submissionA);
const submissionB = submissionRepo.create({
learningObjectHruid: branchingLearningObject.hruid,
learningObjectLanguage: branchingLearningObject.language,
learningObjectVersion: branchingLearningObject.version,
content: '[1]',
onBehalfOf: groupB,
submissionTime: new Date(),
submitter: groupB.members[0],
});
await submissionRepo.save(submissionB);
}); });
describe('fetchLearningPaths', () => { describe('fetchLearningPaths', () => {
it('returns the learning path correctly', async () => { it('returns the learning path correctly', async () => {
const result = await databaseLearningPathProvider.fetchLearningPaths( const result = await databaseLearningPathProvider.fetchLearningPaths([testLearningPath.hruid], testLearningPath.language, 'the source');
[example.learningPath.hruid],
example.learningPath.language,
'the source'
);
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.data?.length).toBe(1); expect(result.data?.length).toBe(1);
const learningObjectsOnPath = ( expectToBeCorrectLearningPath(result.data![0], testLearningPathWithConditions);
await Promise.all(
example.learningPath.nodes.map(async (node) =>
learningObjectService.getLearningObjectById({
hruid: node.learningObjectHruid,
version: node.version,
language: node.language,
})
)
)
).filter((it) => it !== null);
expectToBeCorrectLearningPath(result.data![0], example.learningPath, learningObjectsOnPath);
}); });
it('returns the correct personalized learning path', async () => { it('returns the correct personalized learning path', async () => {
// For student A: // For student A:
let result = await databaseLearningPathProvider.fetchLearningPaths( let result = await databaseLearningPathProvider.fetchLearningPaths(
[persTestData.learningContent.learningPath.hruid], [testLearningPath.hruid],
persTestData.learningContent.learningPath.language, testLearningPath.language,
'the source', 'the source',
{ type: 'student', student: persTestData.studentA } groupA
); );
expect(result.success).toBeTruthy(); expect(result.success).toBeTruthy();
expect(result.data?.length).toBe(1); expect(result.data?.length).toBe(1);
// There should be exactly one branching object // There should be exactly one branching object
let branchingObject = expectBranchingObjectNode(result, persTestData); let branchingObject = expectBranchingObjectNode(result);
expect(branchingObject.transitions.filter((it) => it.next.hruid === persTestData.learningContent.finalObject.hruid).length).toBe(0); // StudentA picked the first option, therefore, there should be no direct path to the final object. expect(branchingObject.transitions.filter((it) => it.next.hruid === finalLearningObject.hruid).length).toBe(0); // StudentA picked the first option, therefore, there should be no direct path to the final object.
expect(branchingObject.transitions.filter((it) => it.next.hruid === persTestData.learningContent.extraExerciseObject.hruid).length).toBe( expect(branchingObject.transitions.filter((it) => it.next.hruid === extraExerciseLearningObject.hruid).length).toBe(1); // There should however be a path to the extra exercise object.
1
); // There should however be a path to the extra exercise object.
// For student B: // For student B:
result = await databaseLearningPathProvider.fetchLearningPaths( result = await databaseLearningPathProvider.fetchLearningPaths([testLearningPath.hruid], testLearningPath.language, 'the source', groupB);
[persTestData.learningContent.learningPath.hruid],
persTestData.learningContent.learningPath.language,
'the source',
{ type: 'student', student: persTestData.studentB }
);
expect(result.success).toBeTruthy(); expect(result.success).toBeTruthy();
expect(result.data?.length).toBe(1); expect(result.data?.length).toBe(1);
// There should still be exactly one branching object // There should still be exactly one branching object
branchingObject = expectBranchingObjectNode(result, persTestData); branchingObject = expectBranchingObjectNode(result);
// However, now the student picks the other option. // However, now the student picks the other option.
expect(branchingObject.transitions.filter((it) => it.next.hruid === persTestData.learningContent.finalObject.hruid).length).toBe(1); // StudentB picked the second option, therefore, there should be a direct path to the final object. expect(branchingObject.transitions.filter((it) => it.next.hruid === finalLearningObject.hruid).length).toBe(1); // StudentB picked the second option, therefore, there should be a direct path to the final object.
expect(branchingObject.transitions.filter((it) => it.next.hruid === persTestData.learningContent.extraExerciseObject.hruid).length).toBe( expect(branchingObject.transitions.filter((it) => it.next.hruid === extraExerciseLearningObject.hruid).length).toBe(0); // There should not be a path anymore to the extra exercise object.
0
); // There should not be a path anymore to the extra exercise object.
}); });
it('returns a non-successful response if a non-existing learning path is queried', async () => { it('returns a non-successful response if a non-existing learning path is queried', async () => {
const result = await databaseLearningPathProvider.fetchLearningPaths( const result = await databaseLearningPathProvider.fetchLearningPaths(
[example.learningPath.hruid], [testLearningPath.hruid],
Language.Abkhazian, // Wrong language Language.Abkhazian, // Wrong language
'the source' 'the source'
); );
@ -233,27 +118,24 @@ describe('DatabaseLearningPathProvider', () => {
describe('searchLearningPaths', () => { describe('searchLearningPaths', () => {
it('returns the correct learning path when queried with a substring of its title', async () => { it('returns the correct learning path when queried with a substring of its title', async () => {
const result = await databaseLearningPathProvider.searchLearningPaths( const result = await databaseLearningPathProvider.searchLearningPaths(testLearningPath.title.substring(2, 6), testLearningPath.language);
example.learningPath.title.substring(2, 6),
example.learningPath.language
);
expect(result.length).toBe(1); expect(result.length).toBe(1);
expect(result[0].title).toBe(example.learningPath.title); expect(result[0].title).toBe(testLearningPath.title);
expect(result[0].description).toBe(example.learningPath.description); expect(result[0].description).toBe(testLearningPath.description);
}); });
it('returns the correct learning path when queried with a substring of the description', async () => { it('returns the correct learning path when queried with a substring of the description', async () => {
const result = await databaseLearningPathProvider.searchLearningPaths( const result = await databaseLearningPathProvider.searchLearningPaths(
example.learningPath.description.substring(5, 12), testLearningPath.description.substring(5, 12),
example.learningPath.language testLearningPath.language
); );
expect(result.length).toBe(1); expect(result.length).toBe(1);
expect(result[0].title).toBe(example.learningPath.title); expect(result[0].title).toBe(testLearningPath.title);
expect(result[0].description).toBe(example.learningPath.description); expect(result[0].description).toBe(testLearningPath.description);
}); });
it('returns an empty result when queried with a text which is not a substring of the title or the description of a learning path', async () => { it('returns an empty result when queried with a text which is not a substring of the title or the description of a learning path', async () => {
const result = await databaseLearningPathProvider.searchLearningPaths( const result = await databaseLearningPathProvider.searchLearningPaths(
'substring which does not occur in the title or the description of a learning object', 'substring which does not occur in the title or the description of a learning object',
example.learningPath.language testLearningPath.language
); );
expect(result.length).toBe(0); expect(result.length).toBe(0);
}); });

View file

@ -1,22 +1,9 @@
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import { LearningPath } from '../../../src/entities/content/learning-path.entity';
import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories';
import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example';
import learningPathExample from '../../test-assets/learning-paths/pn-werking-example';
import learningPathService from '../../../src/services/learning-paths/learning-path-service'; import learningPathService from '../../../src/services/learning-paths/learning-path-service';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { testPartiallyDatabaseAndPartiallyDwengoApiLearningPath } from '../../test_assets/content/learning-paths.testdata';
async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { import { LearningPath as LearningPathDTO } from '@dwengo-1/common/interfaces/learning-content';
const learningObjectRepo = getLearningObjectRepository();
const learningPathRepo = getLearningPathRepository();
const learningObject = learningObjectExample.createLearningObject();
const learningPath = learningPathExample.createLearningPath();
await learningObjectRepo.save(learningObject);
await learningPathRepo.save(learningPath);
return { learningObject, learningPath };
}
const TEST_DWENGO_LEARNING_PATH_HRUID = 'pn_werking'; const TEST_DWENGO_LEARNING_PATH_HRUID = 'pn_werking';
const TEST_DWENGO_LEARNING_PATH_TITLE = 'Werken met notebooks'; const TEST_DWENGO_LEARNING_PATH_TITLE = 'Werken met notebooks';
@ -24,42 +11,49 @@ const TEST_DWENGO_EXCLUSIVE_LEARNING_PATH_SEARCH_QUERY = 'Microscopie';
const TEST_SEARCH_QUERY_EXPECTING_NO_MATCHES = 'su$m8f9usf89ud<p9<U8SDP8UP9'; const TEST_SEARCH_QUERY_EXPECTING_NO_MATCHES = 'su$m8f9usf89ud<p9<U8SDP8UP9';
describe('LearningPathService', () => { describe('LearningPathService', () => {
let example: { learningObject: LearningObject; learningPath: LearningPath }; let testLearningPath: LearningPathDTO;
beforeAll(async () => { beforeAll(async () => {
await setupTestApp(); await setupTestApp();
example = await initExampleData(); testLearningPath = testPartiallyDatabaseAndPartiallyDwengoApiLearningPath;
}); });
describe('fetchLearningPaths', () => { describe('fetchLearningPaths', () => {
it('should return learning paths both from the database and from the Dwengo API', async () => { it('should return learning paths both from the database and from the Dwengo API', async () => {
const result = await learningPathService.fetchLearningPaths( const result = await learningPathService.fetchLearningPaths(
[example.learningPath.hruid, TEST_DWENGO_LEARNING_PATH_HRUID], [testLearningPath.hruid, TEST_DWENGO_LEARNING_PATH_HRUID],
example.learningPath.language, testLearningPath.language as Language,
'the source' 'the source'
); );
expect(result.success).toBeTruthy(); expect(result.success).toBeTruthy();
expect(result.data?.filter((it) => it.hruid === TEST_DWENGO_LEARNING_PATH_HRUID).length).not.toBe(0); expect(result.data?.filter((it) => it.hruid === TEST_DWENGO_LEARNING_PATH_HRUID).length).not.toBe(0);
expect(result.data?.filter((it) => it.hruid === example.learningPath.hruid).length).not.toBe(0); expect(result.data?.filter((it) => it.hruid === testLearningPath.hruid).length).not.toBe(0);
expect(result.data?.find((it) => it.hruid === TEST_DWENGO_LEARNING_PATH_HRUID)?.title).toEqual(TEST_DWENGO_LEARNING_PATH_TITLE); expect(result.data?.find((it) => it.hruid === TEST_DWENGO_LEARNING_PATH_HRUID)?.title).toEqual(TEST_DWENGO_LEARNING_PATH_TITLE);
expect(result.data?.find((it) => it.hruid === example.learningPath.hruid)?.title).toEqual(example.learningPath.title); expect(result.data?.find((it) => it.hruid === testLearningPath.hruid)?.title).toEqual(testLearningPath.title);
}); });
it('should include both the learning objects from the Dwengo API and learning objects from the database in its response', async () => { it('should include both the learning objects from the Dwengo API and learning objects from the database in its response', async () => {
const result = await learningPathService.fetchLearningPaths([example.learningPath.hruid], example.learningPath.language, 'the source'); const result = await learningPathService.fetchLearningPaths(
[testLearningPath.hruid],
testLearningPath.language as Language,
'the source'
);
expect(result.success).toBeTruthy(); expect(result.success).toBeTruthy();
expect(result.data?.length).toBe(1); expect(result.data?.length).toBe(1);
// Should include all the nodes, even those pointing to foreign learning objects. // Should include all the nodes, even those pointing to foreign learning objects.
expect([...result.data![0].nodes.map((it) => it.learningobject_hruid)].sort((a, b) => a.localeCompare(b))).toEqual( expect([...result.data![0].nodes.map((it) => it.learningobject_hruid)].sort((a, b) => a.localeCompare(b))).toEqual(
example.learningPath.nodes.map((it) => it.learningObjectHruid).sort((a, b) => a.localeCompare(b)) testLearningPath.nodes.map((it) => it.learningobject_hruid).sort((a, b) => a.localeCompare(b))
); );
}); });
}); });
describe('searchLearningPath', () => { describe('searchLearningPath', () => {
it('should include both the learning paths from the Dwengo API and those from the database in its response', async () => { it('should include both the learning paths from the Dwengo API and those from the database in its response', async () => {
// This matches the learning object in the database, but definitely also some learning objects in the Dwengo API. // This matches the learning object in the database, but definitely also some learning objects in the Dwengo API.
const result = await learningPathService.searchLearningPaths(example.learningPath.title.substring(2, 3), example.learningPath.language); const result = await learningPathService.searchLearningPaths(
testLearningPath.title.substring(2, 3),
testLearningPath.language as Language
);
// Should find the one from the database // Should find the one from the database
expect(result.filter((it) => it.hruid === example.learningPath.hruid && it.title === example.learningPath.title).length).toBe(1); expect(result.filter((it) => it.hruid === testLearningPath.hruid && it.title === testLearningPath.title).length).toBe(1);
// But should not only find that one. // But should not only find that one.
expect(result.length).not.toBeLessThan(2); expect(result.length).not.toBeLessThan(2);
@ -71,7 +65,7 @@ describe('LearningPathService', () => {
expect(result.length).not.toBe(0); expect(result.length).not.toBe(0);
// But not the example learning path. // But not the example learning path.
expect(result.filter((it) => it.hruid === example.learningPath.hruid && it.title === example.learningPath.title).length).toBe(0); expect(result.filter((it) => it.hruid === testLearningPath.hruid && it.title === testLearningPath.title).length).toBe(0);
}); });
it('should return an empty list if neither the Dwengo API nor the database contains matches', async () => { it('should return an empty list if neither the Dwengo API nor the database contains matches', async () => {
const result = await learningPathService.searchLearningPaths(TEST_SEARCH_QUERY_EXPECTING_NO_MATCHES, Language.Dutch); const result = await learningPathService.searchLearningPaths(TEST_SEARCH_QUERY_EXPECTING_NO_MATCHES, Language.Dutch);

View file

@ -14,6 +14,7 @@ import { makeTestQuestions } from './test_assets/questions/questions.testdata.js
import { makeTestAnswers } from './test_assets/questions/answers.testdata.js'; import { makeTestAnswers } from './test_assets/questions/answers.testdata.js';
import { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js'; import { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js';
import { Collection } from '@mikro-orm/core'; import { Collection } from '@mikro-orm/core';
import { Group } from '../src/entities/assignments/group.entity';
export async function setupTestApp(): Promise<void> { export async function setupTestApp(): Promise<void> {
dotenv.config({ path: '.env.test' }); dotenv.config({ path: '.env.test' });
@ -29,8 +30,8 @@ export async function setupTestApp(): Promise<void> {
const assignments = makeTestAssignemnts(em, classes); const assignments = makeTestAssignemnts(em, classes);
const groups = makeTestGroups(em, students, assignments); const groups = makeTestGroups(em, students, assignments);
assignments[0].groups = new Collection(groups.slice(0, 3)); assignments[0].groups = new Collection<Group>(groups.slice(0, 3));
assignments[1].groups = new Collection(groups.slice(3, 4)); assignments[1].groups = new Collection<Group>(groups.slice(3, 4));
const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes); const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes);
const classJoinRequests = makeTestClassJoinRequests(em, students, classes); const classJoinRequests = makeTestClassJoinRequests(em, students, classes);

View file

@ -1,10 +0,0 @@
import { LearningObjectExample } from './learning-object-example';
import { LearningObject } from '../../../src/entities/content/learning-object.entity';
export function createExampleLearningObjectWithAttachments(example: LearningObjectExample): LearningObject {
const learningObject = example.createLearningObject();
for (const creationFn of Object.values(example.createAttachment)) {
learningObject.attachments.push(creationFn(learningObject));
}
return learningObject;
}

View file

@ -1,32 +0,0 @@
import { LearningObjectExample } from '../learning-object-example';
import { LearningObject } from '../../../../src/entities/content/learning-object.entity';
import { Language } from '@dwengo-1/common/util/language';
import { loadTestAsset } from '../../../test-utils/load-test-asset';
import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type';
import { envVars, getEnvVar } from '../../../../src/util/envVars';
/**
* Create a dummy learning object to be used in tests where multiple learning objects are needed (for example for use
* on a path), but where the precise contents of the learning object are not important.
*/
export function dummyLearningObject(hruid: string, language: Language, title: string): LearningObjectExample {
return {
createLearningObject: (): LearningObject => {
const learningObject = new LearningObject();
learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + hruid;
learningObject.language = language;
learningObject.version = 1;
learningObject.title = title;
learningObject.description = 'Just a dummy learning object for testing purposes';
learningObject.contentType = DwengoContentType.TEXT_PLAIN;
learningObject.content = Buffer.from('Dummy content');
learningObject.returnValue = {
callbackUrl: `/learningObject/${hruid}/submissions`,
callbackSchema: '[]',
};
return learningObject;
},
createAttachment: {},
getHTMLRendering: () => loadTestAsset('learning-objects/dummy/rendering.txt').toString(),
};
}

View file

@ -1,8 +0,0 @@
import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import { Attachment } from '../../../src/entities/content/attachment.entity';
interface LearningObjectExample {
createLearningObject: () => LearningObject;
createAttachment: Record<string, (owner: LearningObject) => Attachment>;
getHTMLRendering: () => string;
}

View file

@ -1,74 +0,0 @@
import { LearningObjectExample } from '../learning-object-example';
import { Language } from '@dwengo-1/common/util/language';
import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type';
import { loadTestAsset } from '../../../test-utils/load-test-asset';
import { LearningObject } from '../../../../src/entities/content/learning-object.entity';
import { Attachment } from '../../../../src/entities/content/attachment.entity';
import { envVars, getEnvVar } from '../../../../src/util/envVars';
import { EducationalGoal } from '../../../../src/entities/content/educational-goal.entity';
import { ReturnValue } from '../../../../src/entities/content/return-value.entity';
const ASSETS_PREFIX = 'learning-objects/pn-werkingnotebooks/';
const example: LearningObjectExample = {
createLearningObject: () => {
const learningObject = new LearningObject();
learningObject.hruid = `${getEnvVar(envVars.UserContentPrefix)}pn_werkingnotebooks`;
learningObject.version = 3;
learningObject.language = Language.Dutch;
learningObject.title = 'Werken met notebooks';
learningObject.description = 'Leren werken met notebooks';
learningObject.keywords = ['Python', 'KIKS', 'Wiskunde', 'STEM', 'AI'];
const educationalGoal1 = new EducationalGoal();
educationalGoal1.source = 'Source';
educationalGoal1.id = 'id';
const educationalGoal2 = new EducationalGoal();
educationalGoal2.source = 'Source2';
educationalGoal2.id = 'id2';
learningObject.educationalGoals = [educationalGoal1, educationalGoal2];
learningObject.admins = [];
learningObject.contentType = DwengoContentType.TEXT_MARKDOWN;
learningObject.teacherExclusive = false;
learningObject.skosConcepts = [
'http://ilearn.ilabt.imec.be/vocab/curr1/s-vaktaal',
'http://ilearn.ilabt.imec.be/vocab/curr1/s-digitale-media-en-toepassingen',
'http://ilearn.ilabt.imec.be/vocab/curr1/s-computers-en-systemen',
];
learningObject.copyright = 'dwengo';
learningObject.license = 'dwengo';
learningObject.estimatedTime = 10;
const returnValue = new ReturnValue();
returnValue.callbackUrl = 'callback_url_example';
returnValue.callbackSchema = '{"att": "test", "att2": "test2"}';
learningObject.returnValue = returnValue;
learningObject.available = true;
learningObject.content = loadTestAsset(`${ASSETS_PREFIX}/content.md`);
return learningObject;
},
createAttachment: {
dwengoLogo: (learningObject) => {
const att = new Attachment();
att.learningObject = learningObject;
att.name = 'dwengo.png';
att.mimeType = 'image/png';
att.content = loadTestAsset(`${ASSETS_PREFIX}/dwengo.png`);
return att;
},
knop: (learningObject) => {
const att = new Attachment();
att.learningObject = learningObject;
att.name = 'Knop.png';
att.mimeType = 'image/png';
att.content = loadTestAsset(`${ASSETS_PREFIX}/Knop.png`);
return att;
},
},
getHTMLRendering: () => loadTestAsset(`${ASSETS_PREFIX}/rendering.txt`).toString(),
};
export default example;

View file

@ -1,2 +0,0 @@
::MC basic::
How are you? {}

View file

@ -1,7 +0,0 @@
<div class="learning-object-gift">
<div id="gift-q1" class="gift-question">
<h2 id="gift-q1-title" class="gift-title">MC basic</h2>
<p id="gift-q1-stem" class="gift-stem">How are you?</p>
<textarea id="gift-q1-answer" class="gift-essay-answer"></textarea>
</div>
</div>

View file

@ -1,28 +0,0 @@
import { LearningObjectExample } from '../learning-object-example';
import { LearningObject } from '../../../../src/entities/content/learning-object.entity';
import { loadTestAsset } from '../../../test-utils/load-test-asset';
import { envVars, getEnvVar } from '../../../../src/util/envVars';
import { Language } from '@dwengo-1/common/util/language';
import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type';
const example: LearningObjectExample = {
createLearningObject: () => {
const learningObject = new LearningObject();
learningObject.hruid = `${getEnvVar(envVars.UserContentPrefix)}test_essay`;
learningObject.language = Language.English;
learningObject.version = 1;
learningObject.title = 'Essay question for testing';
learningObject.description = 'This essay question was only created for testing purposes.';
learningObject.contentType = DwengoContentType.GIFT;
learningObject.returnValue = {
callbackUrl: `/learningObject/${learningObject.hruid}/submissions`,
callbackSchema: '["antwoord vraag 1"]',
};
learningObject.content = loadTestAsset('learning-objects/test-essay/content.txt');
return learningObject;
},
createAttachment: {},
getHTMLRendering: () => loadTestAsset('learning-objects/test-essay/rendering.txt').toString(),
};
export default example;

View file

@ -1,28 +0,0 @@
import { LearningObjectExample } from '../learning-object-example';
import { LearningObject } from '../../../../src/entities/content/learning-object.entity';
import { loadTestAsset } from '../../../test-utils/load-test-asset';
import { envVars, getEnvVar } from '../../../../src/util/envVars';
import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type';
import { Language } from '@dwengo-1/common/util/language';
const example: LearningObjectExample = {
createLearningObject: () => {
const learningObject = new LearningObject();
learningObject.hruid = `${getEnvVar(envVars.UserContentPrefix)}test_multiple_choice`;
learningObject.language = Language.English;
learningObject.version = 1;
learningObject.title = 'Multiple choice question for testing';
learningObject.description = 'This multiple choice question was only created for testing purposes.';
learningObject.contentType = DwengoContentType.GIFT;
learningObject.returnValue = {
callbackUrl: `/learningObject/${learningObject.hruid}/submissions`,
callbackSchema: '["antwoord vraag 1"]',
};
learningObject.content = loadTestAsset('learning-objects/test-multiple-choice/content.txt');
return learningObject;
},
createAttachment: {},
getHTMLRendering: () => loadTestAsset('learning-objects/test-multiple-choice/rendering.txt').toString(),
};
export default example;

View file

@ -1,3 +0,0 @@
interface LearningPathExample {
createLearningPath: () => LearningPath;
}

View file

@ -1,36 +0,0 @@
import { Language } from '@dwengo-1/common/util/language';
import { LearningPathTransition } from '../../../src/entities/content/learning-path-transition.entity';
import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity';
import { LearningPath } from '../../../src/entities/content/learning-path.entity';
export function createLearningPathTransition(
node: LearningPathNode,
transitionNumber: number,
condition: string | null,
to: LearningPathNode
): LearningPathTransition {
const trans = new LearningPathTransition();
trans.node = node;
trans.transitionNumber = transitionNumber;
trans.condition = condition || 'true';
trans.next = to;
return trans;
}
export function createLearningPathNode(
learningPath: LearningPath,
nodeNumber: number,
learningObjectHruid: string,
version: number,
language: Language,
startNode: boolean
): LearningPathNode {
const node = new LearningPathNode();
node.learningPath = learningPath;
node.nodeNumber = nodeNumber;
node.learningObjectHruid = learningObjectHruid;
node.version = version;
node.language = language;
node.startNode = startNode;
return node;
}

View file

@ -1,30 +0,0 @@
import { LearningPath } from '../../../src/entities/content/learning-path.entity';
import { Language } from '@dwengo-1/common/util/language';
import { envVars, getEnvVar } from '../../../src/util/envVars';
import { createLearningPathNode, createLearningPathTransition } from './learning-path-utils';
import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity';
function createNodes(learningPath: LearningPath): LearningPathNode[] {
const nodes = [
createLearningPathNode(learningPath, 0, 'u_pn_werkingnotebooks', 3, Language.Dutch, true),
createLearningPathNode(learningPath, 1, 'pn_werkingnotebooks2', 3, Language.Dutch, false),
createLearningPathNode(learningPath, 2, 'pn_werkingnotebooks3', 3, Language.Dutch, false),
];
nodes[0].transitions.push(createLearningPathTransition(nodes[0], 0, 'true', nodes[1]));
nodes[1].transitions.push(createLearningPathTransition(nodes[1], 0, 'true', nodes[2]));
return nodes;
}
const example: LearningPathExample = {
createLearningPath: () => {
const path = new LearningPath();
path.language = Language.Dutch;
path.hruid = `${getEnvVar(envVars.UserContentPrefix)}pn_werking`;
path.title = 'Werken met notebooks';
path.description = 'Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiënt met de notebooks aan de slag?';
path.nodes = createNodes(path);
return path;
},
};
export default example;

View file

@ -1,80 +0,0 @@
import { LearningPath } from '../../../src/entities/content/learning-path.entity';
import { Language } from '@dwengo-1/common/util/language';
import testMultipleChoiceExample from '../learning-objects/test-multiple-choice/test-multiple-choice-example';
import { dummyLearningObject } from '../learning-objects/dummy/dummy-learning-object-example';
import { createLearningPathNode, createLearningPathTransition } from './learning-path-utils';
import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import { envVars, getEnvVar } from '../../../src/util/envVars';
export interface ConditionTestLearningPathAndLearningObjects {
branchingObject: LearningObject;
extraExerciseObject: LearningObject;
finalObject: LearningObject;
learningPath: LearningPath;
}
export function createConditionTestLearningPathAndLearningObjects(): ConditionTestLearningPathAndLearningObjects {
const learningPath = new LearningPath();
learningPath.hruid = `${getEnvVar(envVars.UserContentPrefix)}test_conditions`;
learningPath.language = Language.English;
learningPath.title = 'Example learning path with conditional transitions';
learningPath.description = 'This learning path was made for the purpose of testing conditional transitions';
const branchingLearningObject = testMultipleChoiceExample.createLearningObject();
const extraExerciseLearningObject = dummyLearningObject(
'test_extra_exercise',
Language.English,
'Extra exercise (for students with difficulties)'
).createLearningObject();
const finalLearningObject = dummyLearningObject(
'test_final_learning_object',
Language.English,
'Final exercise (for everyone)'
).createLearningObject();
const branchingNode = createLearningPathNode(
learningPath,
0,
branchingLearningObject.hruid,
branchingLearningObject.version,
branchingLearningObject.language,
true
);
const extraExerciseNode = createLearningPathNode(
learningPath,
1,
extraExerciseLearningObject.hruid,
extraExerciseLearningObject.version,
extraExerciseLearningObject.language,
false
);
const finalNode = createLearningPathNode(
learningPath,
2,
finalLearningObject.hruid,
finalLearningObject.version,
finalLearningObject.language,
false
);
const transitionToExtraExercise = createLearningPathTransition(
branchingNode,
0,
'$[?(@[0] == 0)]', // The answer to the first question was the first one, which says that it is difficult for the student to follow along.
extraExerciseNode
);
const directTransitionToFinal = createLearningPathTransition(branchingNode, 1, '$[?(@[0] == 1)]', finalNode);
const transitionExtraExerciseToFinal = createLearningPathTransition(extraExerciseNode, 0, 'true', finalNode);
branchingNode.transitions = [transitionToExtraExercise, directTransitionToFinal];
extraExerciseNode.transitions = [transitionExtraExerciseToFinal];
learningPath.nodes = [branchingNode, extraExerciseNode, finalNode];
return {
branchingObject: branchingLearningObject,
finalObject: finalLearningObject,
extraExerciseObject: extraExerciseLearningObject,
learningPath: learningPath,
};
}

View file

@ -1,8 +1,8 @@
import { AssertionError } from 'node:assert'; import { AssertionError } from 'node:assert';
import { LearningObject } from '../../src/entities/content/learning-object.entity'; import { LearningObject } from '../../src/entities/content/learning-object.entity';
import { LearningPath as LearningPathEntity } from '../../src/entities/content/learning-path.entity';
import { expect } from 'vitest'; import { expect } from 'vitest';
import { FilteredLearningObject, LearningPath } from '@dwengo-1/common/interfaces/learning-content'; import { FilteredLearningObject, LearningPath } from '@dwengo-1/common/interfaces/learning-content';
import { RequiredEntityData } from '@mikro-orm/core';
// Ignored properties because they belang for example to the class, not to the entity itself. // Ignored properties because they belang for example to the class, not to the entity itself.
const IGNORE_PROPERTIES = ['parent']; const IGNORE_PROPERTIES = ['parent'];
@ -11,65 +11,56 @@ const IGNORE_PROPERTIES = ['parent'];
* Checks if the actual entity from the database conforms to the entity that was added previously. * Checks if the actual entity from the database conforms to the entity that was added previously.
* @param actual The actual entity retrieved from the database * @param actual The actual entity retrieved from the database
* @param expected The (previously added) entity we would expect to retrieve * @param expected The (previously added) entity we would expect to retrieve
* @param propertyPrefix Prefix to append to property in error messages.
*/ */
export function expectToBeCorrectEntity<T extends object>(actual: { entity: T; name?: string }, expected: { entity: T; name?: string }): void { export function expectToBeCorrectEntity<T extends object>(actual: T, expected: T, propertyPrefix = ''): void {
if (!actual.name) { for (const property in expected) {
actual.name = 'actual'; if (Object.prototype.hasOwnProperty.call(expected, property)) {
} const prefixedProperty = propertyPrefix + property;
if (!expected.name) {
expected.name = 'expected';
}
for (const property in expected.entity) {
if ( if (
property in IGNORE_PROPERTIES && property in IGNORE_PROPERTIES &&
expected.entity[property] !== undefined && // If we don't expect a certain value for a property, we assume it can be filled in by the database however it wants. expected[property] !== undefined && // If we don't expect a certain value for a property, we assume it can be filled in by the database however it wants.
typeof expected.entity[property] !== 'function' // Functions obviously are not persisted via the database typeof expected[property] !== 'function' // Functions obviously are not persisted via the database
) { ) {
if (!Object.prototype.hasOwnProperty.call(actual.entity, property)) { if (!Object.prototype.hasOwnProperty.call(actual, property)) {
throw new AssertionError({ throw new AssertionError({
message: `${expected.name} has defined property ${property}, but ${actual.name} is missing it.`, message: `Expected property ${prefixedProperty}, but it is missing.`,
}); });
} }
if (typeof expected.entity[property] === 'boolean') { if (typeof expected[property] === 'boolean') {
// Sometimes, booleans get represented by numbers 0 and 1 in the objects actual from the database. // Sometimes, booleans get represented by numbers 0 and 1 in the objects actual from the database.
if (Boolean(expected.entity[property]) !== Boolean(actual.entity[property])) { if (Boolean(expected[property]) !== Boolean(actual[property])) {
throw new AssertionError({ throw new AssertionError({
message: `${property} was ${expected.entity[property]} in ${expected.name}, message: `Expected ${prefixedProperty} to be ${expected[property]},
but ${actual.entity[property]} (${Boolean(expected.entity[property])}) in ${actual.name}`, but was ${actual[property]} (${Boolean(expected[property])}).`,
}); });
} }
} else if (typeof expected.entity[property] !== typeof actual.entity[property]) { } else if (typeof expected[property] !== typeof actual[property]) {
throw new AssertionError({ throw new AssertionError({
message: `${property} has type ${typeof expected.entity[property]} in ${expected.name}, but type ${typeof actual.entity[property]} in ${actual.name}.`, message:
`${prefixedProperty} was expected to have type ${typeof expected[property]},` +
`but had type ${typeof actual[property]}.`,
}); });
} else if (typeof expected.entity[property] === 'object') { } else if (typeof expected[property] === 'object') {
expectToBeCorrectEntity( expectToBeCorrectEntity(actual[property] as object, expected[property] as object, property);
{
name: actual.name + '.' + property,
entity: actual.entity[property] as object,
},
{
name: expected.name + '.' + property,
entity: expected.entity[property] as object,
}
);
} else { } else {
if (expected.entity[property] !== actual.entity[property]) { if (expected[property] !== actual[property]) {
throw new AssertionError({ throw new AssertionError({
message: `${property} was ${expected.entity[property]} in ${expected.name}, but ${actual.entity[property]} in ${actual.name}`, message: `${prefixedProperty} was expected to be ${expected[property]}, ` + `but was ${actual[property]}.`,
}); });
} }
} }
} }
} }
} }
}
/** /**
* Checks that filtered is the correct representation of original as FilteredLearningObject. * Checks that filtered is the correct representation of original as FilteredLearningObject.
* @param filtered the representation as FilteredLearningObject * @param filtered the representation as FilteredLearningObject
* @param original the original entity added to the database * @param original the data of the entity in the database that was filtered.
*/ */
export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearningObject, original: LearningObject): void { export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearningObject, original: RequiredEntityData<LearningObject>): void {
expect(filtered.uuid).toEqual(original.uuid); expect(filtered.uuid).toEqual(original.uuid);
expect(filtered.version).toEqual(original.version); expect(filtered.version).toEqual(original.version);
expect(filtered.language).toEqual(original.language); expect(filtered.language).toEqual(original.language);
@ -97,54 +88,55 @@ export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearni
* is a correct representation of the given learning path entity. * is a correct representation of the given learning path entity.
* *
* @param learningPath The learning path returned by the retriever, service or endpoint * @param learningPath The learning path returned by the retriever, service or endpoint
* @param expectedEntity The expected entity * @param expected The learning path that should have been returned.
* @param learningObjectsOnPath The learning objects on LearningPath. Necessary since some information in
* the learning path returned from the API endpoint
*/ */
export function expectToBeCorrectLearningPath( export function expectToBeCorrectLearningPath(learningPath: LearningPath, expected: LearningPath): void {
learningPath: LearningPath, expect(learningPath.hruid).toEqual(expected.hruid);
expectedEntity: LearningPathEntity, expect(learningPath.language).toEqual(expected.language);
learningObjectsOnPath: FilteredLearningObject[] expect(learningPath.description).toEqual(expected.description);
): void { expect(learningPath.title).toEqual(expected.title);
expect(learningPath.hruid).toEqual(expectedEntity.hruid);
expect(learningPath.language).toEqual(expectedEntity.language);
expect(learningPath.description).toEqual(expectedEntity.description);
expect(learningPath.title).toEqual(expectedEntity.title);
const keywords = new Set(learningObjectsOnPath.flatMap((it) => it.keywords || [])); expect(new Set(learningPath.keywords.split(' '))).toEqual(new Set(learningPath.keywords.split(' ')));
expect(new Set(learningPath.keywords.split(' '))).toEqual(keywords);
const targetAges = new Set(learningObjectsOnPath.flatMap((it) => it.targetAges || [])); expect(new Set(learningPath.target_ages)).toEqual(new Set(expected.target_ages));
expect(new Set(learningPath.target_ages)).toEqual(targetAges); expect(learningPath.min_age).toEqual(Math.min(...expected.target_ages));
expect(learningPath.min_age).toEqual(Math.min(...targetAges)); expect(learningPath.max_age).toEqual(Math.max(...expected.target_ages));
expect(learningPath.max_age).toEqual(Math.max(...targetAges));
expect(learningPath.num_nodes).toEqual(expectedEntity.nodes.length); expect(learningPath.num_nodes).toEqual(expected.nodes.length);
expect(learningPath.image || null).toEqual(expectedEntity.image); expect(learningPath.image ?? null).toEqual(expected.image ?? null);
const expectedLearningPathNodes = new Map( for (const node of expected.nodes) {
expectedEntity.nodes.map((node) => [ const correspondingNode = learningPath.nodes.find(
{ learningObjectHruid: node.learningObjectHruid, language: node.language, version: node.version }, (it) => node.learningobject_hruid === it.learningobject_hruid && node.language === it.language && node.version === it.version
{ startNode: node.startNode, transitions: node.transitions },
])
); );
expect(correspondingNode).toBeTruthy();
expect(Boolean(correspondingNode!.start_node)).toEqual(Boolean(node.start_node));
for (const node of learningPath.nodes) { for (const transition of node.transitions) {
const nodeKey = { const correspondingTransition = correspondingNode!.transitions.find(
learningObjectHruid: node.learningobject_hruid, (it) =>
language: node.language, it.next.hruid === transition.next.hruid &&
version: node.version, it.next.language === transition.next.language &&
}; it.next.version === transition.next.version
expect(expectedLearningPathNodes.keys()).toContainEqual(nodeKey);
const expectedNode = [...expectedLearningPathNodes.entries()].find(
([key, _]) => key.learningObjectHruid === nodeKey.learningObjectHruid && key.language === node.language && key.version === node.version
)![1];
expect(node.start_node).toEqual(expectedNode.startNode);
expect(new Set(node.transitions.map((it) => it.next.hruid))).toEqual(
new Set(expectedNode.transitions.map((it) => it.next.learningObjectHruid))
); );
expect(new Set(node.transitions.map((it) => it.next.language))).toEqual(new Set(expectedNode.transitions.map((it) => it.next.language))); expect(correspondingTransition).toBeTruthy();
expect(new Set(node.transitions.map((it) => it.next.version))).toEqual(new Set(expectedNode.transitions.map((it) => it.next.version)));
} }
} }
}
/**
* Expect that the given result is a singleton list with exactly the given element.
*/
export function expectToHaveFoundPrecisely<T extends object>(expected: T, result: T[]): void {
expect(result).toHaveProperty('length');
expect(result.length).toBe(1);
expectToBeCorrectEntity(result[0], expected);
}
/**
* Expect that the given result is an empty list.
*/
export function expectToHaveFoundNothing<T>(result: T[]): void {
expect(result).toHaveProperty('length');
expect(result.length).toBe(0);
}

View file

@ -0,0 +1,10 @@
import { RequiredEntityData } from '@mikro-orm/core';
import { loadTestAsset } from './load-test-asset';
import { LearningObject } from '../../src/entities/content/learning-object.entity';
import { envVars, getEnvVar } from '../../src/util/envVars';
export function getHtmlRenderingForTestLearningObject(learningObject: RequiredEntityData<LearningObject>): string {
const userPrefix = getEnvVar(envVars.UserContentPrefix);
const cleanedHruid = learningObject.hruid.startsWith(userPrefix) ? learningObject.hruid.substring(userPrefix.length) : learningObject.hruid;
return loadTestAsset(`/content/learning-object-resources/${cleanedHruid}/rendering.txt`).toString();
}

View file

@ -1,10 +1,14 @@
import fs from 'fs'; import fs from 'fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url';
const fileName = fileURLToPath(import.meta.url);
const dirName = path.dirname(fileName);
/** /**
* Load the asset at the given path. * Load the asset at the given path.
* @param relPath Path of the asset relative to the test-assets folder. * @param relPath Path of the asset relative to the test-assets folder.
*/ */
export function loadTestAsset(relPath: string): Buffer { export function loadTestAsset(relPath: string): Buffer {
return fs.readFileSync(path.resolve(__dirname, `../test-assets/${relPath}`)); return fs.readFileSync(path.resolve(dirName, `../test_assets/${relPath}`));
} }

View file

@ -2,11 +2,13 @@ import { EntityManager } from '@mikro-orm/core';
import { Assignment } from '../../../src/entities/assignments/assignment.entity'; import { Assignment } from '../../../src/entities/assignments/assignment.entity';
import { Class } from '../../../src/entities/classes/class.entity'; import { Class } from '../../../src/entities/classes/class.entity';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { testLearningPathWithConditions } from '../content/learning-paths.testdata';
import { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata';
export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] { export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] {
const assignment01 = em.create(Assignment, { assignment01 = em.create(Assignment, {
id: 21000,
within: classes[0], within: classes[0],
id: 1,
title: 'dire straits', title: 'dire straits',
description: 'reading', description: 'reading',
learningPathHruid: 'id02', learningPathHruid: 'id02',
@ -14,9 +16,9 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
groups: [], groups: [],
}); });
const assignment02 = em.create(Assignment, { assignment02 = em.create(Assignment, {
id: 21001,
within: classes[1], within: classes[1],
id: 2,
title: 'tool', title: 'tool',
description: 'reading', description: 'reading',
learningPathHruid: 'id01', learningPathHruid: 'id01',
@ -24,9 +26,9 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
groups: [], groups: [],
}); });
const assignment03 = em.create(Assignment, { assignment03 = em.create(Assignment, {
id: 21002,
within: classes[0], within: classes[0],
id: 3,
title: 'delete', title: 'delete',
description: 'will be deleted', description: 'will be deleted',
learningPathHruid: 'id02', learningPathHruid: 'id02',
@ -34,9 +36,9 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
groups: [], groups: [],
}); });
const assignment04 = em.create(Assignment, { assignment04 = em.create(Assignment, {
id: 21003,
within: classes[0], within: classes[0],
id: 4,
title: 'another assignment', title: 'another assignment',
description: 'with a description', description: 'with a description',
learningPathHruid: 'id01', learningPathHruid: 'id01',
@ -44,5 +46,41 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
groups: [], groups: [],
}); });
return [assignment01, assignment02, assignment03, assignment04]; conditionalPathAssignment = em.create(Assignment, {
within: getClassWithTestleerlingAndTestleerkracht(),
id: 21004,
title: 'Assignment: Conditional Learning Path',
description: 'You have to do the testing learning path with a condition.',
learningPathHruid: testLearningPathWithConditions.hruid,
learningPathLanguage: testLearningPathWithConditions.language as Language,
groups: [],
});
return [assignment01, assignment02, assignment03, assignment04, conditionalPathAssignment];
}
let assignment01: Assignment;
let assignment02: Assignment;
let assignment03: Assignment;
let assignment04: Assignment;
let conditionalPathAssignment: Assignment;
export function getAssignment01(): Assignment {
return assignment01;
}
export function getAssignment02(): Assignment {
return assignment02;
}
export function getAssignment03(): Assignment {
return assignment03;
}
export function getAssignment04(): Assignment {
return assignment04;
}
export function getConditionalPathAssignment(): Assignment {
return conditionalPathAssignment;
} }

View file

@ -2,15 +2,17 @@ import { EntityManager } from '@mikro-orm/core';
import { Group } from '../../../src/entities/assignments/group.entity'; import { Group } from '../../../src/entities/assignments/group.entity';
import { Assignment } from '../../../src/entities/assignments/assignment.entity'; import { Assignment } from '../../../src/entities/assignments/assignment.entity';
import { Student } from '../../../src/entities/users/student.entity'; import { Student } from '../../../src/entities/users/student.entity';
import { getConditionalPathAssignment } from './assignments.testdata';
import { getTestleerling1 } from '../users/students.testdata';
export function makeTestGroups(em: EntityManager, students: Student[], assignments: Assignment[]): Group[] { export function makeTestGroups(em: EntityManager, students: Student[], assignments: Assignment[]): Group[] {
/* /*
* Group #1 for Assignment #1 in class 'id01' * Group #1 for Assignment #1 in class 'id01'
* => Assigned to do learning path 'id02' * => Assigned to do learning path 'id02'
*/ */
const group01 = em.create(Group, { group01 = em.create(Group, {
assignment: assignments[0], assignment: assignments[0],
groupNumber: 1, groupNumber: 21001,
members: students.slice(0, 2), members: students.slice(0, 2),
}); });
@ -18,9 +20,9 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen
* Group #2 for Assignment #1 in class 'id01' * Group #2 for Assignment #1 in class 'id01'
* => Assigned to do learning path 'id02' * => Assigned to do learning path 'id02'
*/ */
const group02 = em.create(Group, { group02 = em.create(Group, {
assignment: assignments[0], assignment: assignments[0],
groupNumber: 2, groupNumber: 21002,
members: students.slice(2, 4), members: students.slice(2, 4),
}); });
@ -28,9 +30,9 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen
* Group #3 for Assignment #1 in class 'id01' * Group #3 for Assignment #1 in class 'id01'
* => Assigned to do learning path 'id02' * => Assigned to do learning path 'id02'
*/ */
const group03 = em.create(Group, { group03 = em.create(Group, {
assignment: assignments[0], assignment: assignments[0],
groupNumber: 3, groupNumber: 21003,
members: students.slice(4, 6), members: students.slice(4, 6),
}); });
@ -38,9 +40,9 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen
* Group #4 for Assignment #2 in class 'id02' * Group #4 for Assignment #2 in class 'id02'
* => Assigned to do learning path 'id01' * => Assigned to do learning path 'id01'
*/ */
const group04 = em.create(Group, { group04 = em.create(Group, {
assignment: assignments[1], assignment: assignments[1],
groupNumber: 4, groupNumber: 21004,
members: students.slice(3, 4), members: students.slice(3, 4),
}); });
@ -48,11 +50,51 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen
* Group #5 for Assignment #4 in class 'id01' * Group #5 for Assignment #4 in class 'id01'
* => Assigned to do learning path 'id01' * => Assigned to do learning path 'id01'
*/ */
const group05 = em.create(Group, { group05 = em.create(Group, {
assignment: assignments[3], assignment: assignments[3],
groupNumber: 1, groupNumber: 21001,
members: students.slice(0, 2), members: students.slice(0, 2),
}); });
return [group01, group02, group03, group04, group05]; /**
* Group 1 for the assignment of the testing learning path with conditions.
*/
group1ConditionalLearningPath = em.create(Group, {
assignment: getConditionalPathAssignment(),
groupNumber: 1,
members: [getTestleerling1()],
});
return [group01, group02, group03, group04, group05, group1ConditionalLearningPath];
}
let group01: Group;
let group02: Group;
let group03: Group;
let group04: Group;
let group05: Group;
let group1ConditionalLearningPath: Group;
export function getTestGroup01(): Group {
return group01;
}
export function getTestGroup02(): Group {
return group02;
}
export function getTestGroup03(): Group {
return group03;
}
export function getTestGroup04(): Group {
return group04;
}
export function getTestGroup05(): Group {
return group05;
}
export function getGroup1ConditionalLearningPath(): Group {
return group1ConditionalLearningPath;
} }

View file

@ -2,12 +2,14 @@ import { EntityManager } from '@mikro-orm/core';
import { Class } from '../../../src/entities/classes/class.entity'; import { Class } from '../../../src/entities/classes/class.entity';
import { Student } from '../../../src/entities/users/student.entity'; import { Student } from '../../../src/entities/users/student.entity';
import { Teacher } from '../../../src/entities/users/teacher.entity'; import { Teacher } from '../../../src/entities/users/teacher.entity';
import { getTestleerkracht1 } from '../users/teachers.testdata';
import { getTestleerling1 } from '../users/students.testdata';
export function makeTestClasses(em: EntityManager, students: Student[], teachers: Teacher[]): Class[] { export function makeTestClasses(em: EntityManager, students: Student[], teachers: Teacher[]): Class[] {
const studentsClass01 = students.slice(0, 8); const studentsClass01 = students.slice(0, 8);
const teacherClass01: Teacher[] = teachers.slice(4, 5); const teacherClass01: Teacher[] = teachers.slice(4, 5);
const class01 = em.create(Class, { class01 = em.create(Class, {
classId: '8764b861-90a6-42e5-9732-c0d9eb2f55f9', classId: '8764b861-90a6-42e5-9732-c0d9eb2f55f9',
displayName: 'class01', displayName: 'class01',
teachers: teacherClass01, teachers: teacherClass01,
@ -17,7 +19,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
const studentsClass02: Student[] = students.slice(0, 2).concat(students.slice(3, 4)); const studentsClass02: Student[] = students.slice(0, 2).concat(students.slice(3, 4));
const teacherClass02: Teacher[] = teachers.slice(1, 2); const teacherClass02: Teacher[] = teachers.slice(1, 2);
const class02 = em.create(Class, { class02 = em.create(Class, {
classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89',
displayName: 'class02', displayName: 'class02',
teachers: teacherClass02, teachers: teacherClass02,
@ -27,7 +29,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
const studentsClass03: Student[] = students.slice(1, 4); const studentsClass03: Student[] = students.slice(1, 4);
const teacherClass03: Teacher[] = teachers.slice(2, 3); const teacherClass03: Teacher[] = teachers.slice(2, 3);
const class03 = em.create(Class, { class03 = em.create(Class, {
classId: '80dcc3e0-1811-4091-9361-42c0eee91cfa', classId: '80dcc3e0-1811-4091-9361-42c0eee91cfa',
displayName: 'class03', displayName: 'class03',
teachers: teacherClass03, teachers: teacherClass03,
@ -37,12 +39,45 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
const studentsClass04: Student[] = students.slice(0, 2); const studentsClass04: Student[] = students.slice(0, 2);
const teacherClass04: Teacher[] = teachers.slice(2, 3); const teacherClass04: Teacher[] = teachers.slice(2, 3);
const class04 = em.create(Class, { class04 = em.create(Class, {
classId: '33d03536-83b8-4880-9982-9bbf2f908ddf', classId: '33d03536-83b8-4880-9982-9bbf2f908ddf',
displayName: 'class04', displayName: 'class04',
teachers: teacherClass04, teachers: teacherClass04,
students: studentsClass04, students: studentsClass04,
}); });
return [class01, class02, class03, class04]; classWithTestleerlingAndTestleerkracht = em.create(Class, {
classId: 'a75298b5-18aa-471d-8eeb-5d77eb989393',
displayName: 'Testklasse',
teachers: [getTestleerkracht1()],
students: [getTestleerling1()],
});
return [class01, class02, class03, class04, classWithTestleerlingAndTestleerkracht];
}
let class01: Class;
let class02: Class;
let class03: Class;
let class04: Class;
let classWithTestleerlingAndTestleerkracht: Class;
export function getClass01(): Class {
return class01;
}
export function getClass02(): Class {
return class02;
}
export function getClass03(): Class {
return class03;
}
export function getClass04(): Class {
return class04;
}
export function getClassWithTestleerlingAndTestleerkracht(): Class {
return classWithTestleerlingAndTestleerkracht;
} }

View file

@ -0,0 +1,2 @@
::Reflection::
Reflect on this learning path. What have you learned today? {}

View file

@ -0,0 +1,7 @@
<div class="learning-object-gift">
<div id="gift-q1" class="gift-question gift-question-type-Essay">
<h2 id="gift-q1-title" class="gift-title">Reflection</h2>
<p id="gift-q1-stem" class="gift-stem">Reflect on this learning path. What have you learned today?</p>
<textarea id="gift-q1-answer" class="gift-essay-answer"></textarea>
</div>
</div>

View file

@ -1,5 +1,5 @@
::MC basic:: ::Self-evaluation::
Are you following along well with the class? { Are you following along well? {
~No, it's very difficult to follow along. ~No, it's very difficult to follow along.
=Yes, no problem! =Yes, no problem!
} }

View file

@ -1,14 +1,14 @@
<div class="learning-object-gift"> <div class="learning-object-gift">
<div id="gift-q1" class="gift-question"> <div id="gift-q1" class="gift-question gift-question-type-MC">
<h2 id="gift-q1-title" class="gift-title">MC basic</h2> <h2 id="gift-q1-title" class="gift-title">Self-evaluation</h2>
<p id="gift-q1-stem" class="gift-stem">Are you following along well with the class?</p> <p id="gift-q1-stem" class="gift-stem">Are you following along well?</p>
<div class="gift-choice-div"> <div class="gift-choice-div">
<input value="0" name="gift-q1-choices" id="gift-q1-choice-0" type="radio"> <input value="0" name="gift-q1-choices" id="gift-q1-choice-0" type="radio">
<label for="gift-q1-choice-0">[object Object]</label> <label for="gift-q1-choice-0">No, it's very difficult to follow along.</label>
</div> </div>
<div class="gift-choice-div"> <div class="gift-choice-div">
<input value="1" name="gift-q1-choices" id="gift-q1-choice-1" type="radio"> <input value="1" name="gift-q1-choices" id="gift-q1-choice-1" type="radio">
<label for="gift-q1-choice-1">[object Object]</label> <label for="gift-q1-choice-1">Yes, no problem!</label>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,16 +1,49 @@
import { EntityManager } from '@mikro-orm/core'; import { EntityManager, RequiredEntityData } from '@mikro-orm/core';
import { LearningObject } from '../../../src/entities/content/learning-object.entity'; import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { DwengoContentType } from '../../../src/services/learning-objects/processing/content-type'; import { DwengoContentType } from '../../../src/services/learning-objects/processing/content-type';
import { ReturnValue } from '../../../src/entities/content/return-value.entity'; import { ReturnValue } from '../../../src/entities/content/return-value.entity';
import { envVars, getEnvVar } from '../../../src/util/envVars';
import { loadTestAsset } from '../../test-utils/load-test-asset';
import { v4 } from 'uuid';
export function makeTestLearningObjects(em: EntityManager): LearningObject[] { export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
const returnValue: ReturnValue = new ReturnValue(); const returnValue: ReturnValue = new ReturnValue();
returnValue.callbackSchema = ''; returnValue.callbackSchema = '';
returnValue.callbackUrl = ''; returnValue.callbackUrl = '';
const learningObject01 = em.create(LearningObject, { const learningObject01 = em.create(LearningObject, testLearningObject01);
hruid: 'id01', const learningObject02 = em.create(LearningObject, testLearningObject02);
const learningObject03 = em.create(LearningObject, testLearningObject03);
const learningObject04 = em.create(LearningObject, testLearningObject04);
const learningObject05 = em.create(LearningObject, testLearningObject05);
const learningObjectMultipleChoice = em.create(LearningObject, testLearningObjectMultipleChoice);
const learningObjectEssayQuestion = em.create(LearningObject, testLearningObjectEssayQuestion);
const learningObjectPnNotebooks = em.create(LearningObject, testLearningObjectPnNotebooks);
return [
learningObject01,
learningObject02,
learningObject03,
learningObject04,
learningObject05,
learningObjectMultipleChoice,
learningObjectEssayQuestion,
learningObjectPnNotebooks,
];
}
export function createReturnValue(): ReturnValue {
const returnValue: ReturnValue = new ReturnValue();
returnValue.callbackSchema = '[]';
returnValue.callbackUrl = '%SUBMISSION%';
return returnValue;
}
export const testLearningObject01: RequiredEntityData<LearningObject> = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}id01`,
language: Language.English, language: Language.English,
version: 1, version: 1,
admins: [], admins: [],
@ -18,21 +51,23 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
description: 'debute', description: 'debute',
contentType: DwengoContentType.TEXT_MARKDOWN, contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [], keywords: [],
uuid: v4(),
targetAges: [16, 17, 18],
teacherExclusive: false, teacherExclusive: false,
skosConcepts: [], skosConcepts: [],
educationalGoals: [], educationalGoals: [],
copyright: '', copyright: '',
license: '', license: '',
estimatedTime: 45, estimatedTime: 45,
returnValue: returnValue, returnValue: createReturnValue(),
available: true, available: true,
contentLocation: '', contentLocation: '',
attachments: [], attachments: [],
content: Buffer.from("there's a shadow just behind me, shrouding every step i take, making every promise empty pointing every finger at me"), content: Buffer.from("there's a shadow just behind me, shrouding every step i take, making every promise empty pointing every finger at me"),
}); };
const learningObject02 = em.create(LearningObject, { export const testLearningObject02: RequiredEntityData<LearningObject> = {
hruid: 'id02', hruid: `${getEnvVar(envVars.UserContentPrefix)}id02`,
language: Language.English, language: Language.English,
version: 1, version: 1,
admins: [], admins: [],
@ -46,17 +81,17 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
copyright: '', copyright: '',
license: '', license: '',
estimatedTime: 80, estimatedTime: 80,
returnValue: returnValue, returnValue: createReturnValue(),
available: true, available: true,
contentLocation: '', contentLocation: '',
attachments: [], attachments: [],
content: Buffer.from( content: Buffer.from(
"I've been crawling on my belly clearing out what could've been I've been wallowing in my own confused and insecure delusions" "I've been crawling on my belly clearing out what could've been I've been wallowing in my own confused and insecure delusions"
), ),
}); };
const learningObject03 = em.create(LearningObject, { export const testLearningObject03: RequiredEntityData<LearningObject> = {
hruid: 'id03', hruid: `${getEnvVar(envVars.UserContentPrefix)}id03`,
language: Language.English, language: Language.English,
version: 1, version: 1,
admins: [], admins: [],
@ -70,7 +105,7 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
copyright: '', copyright: '',
license: '', license: '',
estimatedTime: 55, estimatedTime: 55,
returnValue: returnValue, returnValue: createReturnValue(),
available: true, available: true,
contentLocation: '', contentLocation: '',
attachments: [], attachments: [],
@ -80,10 +115,10 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
come back and see me later next patient please \ come back and see me later next patient please \
send in another victim of industrial disease' send in another victim of industrial disease'
), ),
}); };
const learningObject04 = em.create(LearningObject, { export const testLearningObject04: RequiredEntityData<LearningObject> = {
hruid: 'id04', hruid: `${getEnvVar(envVars.UserContentPrefix)}id04`,
language: Language.English, language: Language.English,
version: 1, version: 1,
admins: [], admins: [],
@ -97,7 +132,7 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
copyright: '', copyright: '',
license: '', license: '',
estimatedTime: 55, estimatedTime: 55,
returnValue: returnValue, returnValue: createReturnValue(),
available: true, available: true,
contentLocation: '', contentLocation: '',
attachments: [], attachments: [],
@ -107,10 +142,10 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
I had the one-arm bandit fever \ I had the one-arm bandit fever \
There was an arrow through my heart and my soul' There was an arrow through my heart and my soul'
), ),
}); };
const learningObject05 = em.create(LearningObject, { export const testLearningObject05: RequiredEntityData<LearningObject> = {
hruid: 'id05', hruid: `${getEnvVar(envVars.UserContentPrefix)}id05`,
language: Language.English, language: Language.English,
version: 1, version: 1,
admins: [], admins: [],
@ -124,12 +159,103 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
copyright: '', copyright: '',
license: '', license: '',
estimatedTime: 55, estimatedTime: 55,
returnValue: returnValue, returnValue: createReturnValue(),
available: true, available: true,
contentLocation: '', contentLocation: '',
attachments: [], attachments: [],
content: Buffer.from('calling Elvis, is anybody home, calling elvis, I am here all alone'), content: Buffer.from('calling Elvis, is anybody home, calling elvis, I am here all alone'),
}); };
return [learningObject01, learningObject02, learningObject03, learningObject04, learningObject05]; export const testLearningObjectMultipleChoice: RequiredEntityData<LearningObject> = {
} hruid: `${getEnvVar(envVars.UserContentPrefix)}test_multiple_choice`,
language: Language.English,
version: 1,
title: 'Self-evaluation',
description: "Time to evaluate how well you understand what you've learned so far.",
keywords: ['test'],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: 'Groep 1 SEL-2 2025',
license: 'CC0',
difficulty: 1,
estimatedTime: 1,
attachments: [],
available: true,
targetAges: [10, 11, 12, 13, 14, 15, 16, 17, 18],
admins: [],
contentType: DwengoContentType.GIFT,
content: loadTestAsset('content/learning-object-resources/test_multiple_choice/content.txt'),
returnValue: {
callbackUrl: `%SUBMISSION%`,
callbackSchema: '["antwoord vraag 1"]',
},
};
export const testLearningObjectEssayQuestion: RequiredEntityData<LearningObject> = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}test_essay_question`,
language: Language.English,
version: 1,
title: 'Reflection',
description: 'Reflect on your learning progress.',
keywords: ['test'],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: 'Groep 1 SEL-2 2025',
license: 'CC0',
difficulty: 1,
estimatedTime: 1,
attachments: [],
available: true,
targetAges: [10, 11, 12, 13, 14, 15, 16, 17, 18],
admins: [],
contentType: DwengoContentType.GIFT,
content: loadTestAsset('content/learning-object-resources/test_essay_question/content.txt'),
returnValue: {
callbackUrl: `%SUBMISSION%`,
callbackSchema: '["antwoord vraag 1"]',
},
};
export const testLearningObjectPnNotebooks: RequiredEntityData<LearningObject> = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}pn_werkingnotebooks`,
version: 3,
language: Language.Dutch,
title: 'Werken met notebooks',
description: 'Leren werken met notebooks',
keywords: ['Python', 'KIKS', 'Wiskunde', 'STEM', 'AI'],
targetAges: [14, 15, 16, 17, 18],
admins: [],
copyright: 'dwengo',
educationalGoals: [],
license: 'dwengo',
contentType: DwengoContentType.TEXT_MARKDOWN,
difficulty: 3,
estimatedTime: 10,
uuid: '2adf9929-b424-4650-bf60-186f730d38ab',
teacherExclusive: false,
skosConcepts: [
'http://ilearn.ilabt.imec.be/vocab/curr1/s-vaktaal',
'http://ilearn.ilabt.imec.be/vocab/curr1/s-digitale-media-en-toepassingen',
'http://ilearn.ilabt.imec.be/vocab/curr1/s-computers-en-systemen',
],
attachments: [
{
name: 'dwengo.png',
mimeType: 'image/png',
content: loadTestAsset('/content/learning-object-resources/pn_werkingnotebooks/dwengo.png'),
},
{
name: 'Knop.png',
mimeType: 'image/png',
content: loadTestAsset('/content/learning-object-resources/pn_werkingnotebooks/Knop.png'),
},
],
available: false,
content: loadTestAsset('/content/learning-object-resources/pn_werkingnotebooks/content.md'),
returnValue: {
callbackUrl: '%SUBMISSION%',
callbackSchema: '[]',
},
};

View file

@ -1,100 +1,236 @@
import { EntityManager } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/core';
import { LearningPath } from '../../../src/entities/content/learning-path.entity'; import { LearningPath } from '../../../src/entities/content/learning-path.entity';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { LearningPathTransition } from '../../../src/entities/content/learning-path-transition.entity'; import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service';
import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; import { envVars, getEnvVar } from '../../../src/util/envVars';
import { LearningPath as LearningPathDTO } from '@dwengo-1/common/interfaces/learning-content';
import {
testLearningObject01,
testLearningObject02,
testLearningObject03,
testLearningObject04,
testLearningObject05,
testLearningObjectEssayQuestion,
testLearningObjectMultipleChoice,
testLearningObjectPnNotebooks,
} from './learning-objects.testdata';
export function makeTestLearningPaths(em: EntityManager): LearningPath[] { export function makeTestLearningPaths(_em: EntityManager): LearningPath[] {
const learningPathNode01: LearningPathNode = new LearningPathNode(); const learningPath01 = mapToLearningPath(testLearningPath01, []);
const learningPathNode02: LearningPathNode = new LearningPathNode(); const learningPath02 = mapToLearningPath(testLearningPath02, []);
const learningPathNode03: LearningPathNode = new LearningPathNode();
const learningPathNode04: LearningPathNode = new LearningPathNode();
const learningPathNode05: LearningPathNode = new LearningPathNode();
const transitions01: LearningPathTransition = new LearningPathTransition(); const partiallyDatabasePartiallyDwengoApiLearningPath = mapToLearningPath(testPartiallyDatabaseAndPartiallyDwengoApiLearningPath, []);
const transitions02: LearningPathTransition = new LearningPathTransition(); const learningPathWithConditions = mapToLearningPath(testLearningPathWithConditions, []);
const transitions03: LearningPathTransition = new LearningPathTransition();
const transitions04: LearningPathTransition = new LearningPathTransition();
const transitions05: LearningPathTransition = new LearningPathTransition();
transitions01.condition = 'true'; return [learningPath01, learningPath02, partiallyDatabasePartiallyDwengoApiLearningPath, learningPathWithConditions];
transitions01.next = learningPathNode02; }
transitions02.condition = 'true'; const nowString = new Date().toString();
transitions02.next = learningPathNode02;
transitions03.condition = 'true'; export const testLearningPath01: LearningPathDTO = {
transitions03.next = learningPathNode04; keywords: 'test',
target_ages: [16, 17, 18],
transitions04.condition = 'true'; hruid: `${getEnvVar(envVars.UserContentPrefix)}id01`,
transitions04.next = learningPathNode05;
transitions05.condition = 'true';
transitions05.next = learningPathNode05;
learningPathNode01.instruction = '';
learningPathNode01.language = Language.English;
learningPathNode01.learningObjectHruid = 'id01';
learningPathNode01.startNode = true;
learningPathNode01.transitions = [transitions01];
learningPathNode01.version = 1;
learningPathNode02.instruction = '';
learningPathNode02.language = Language.English;
learningPathNode02.learningObjectHruid = 'id02';
learningPathNode02.startNode = false;
learningPathNode02.transitions = [transitions02];
learningPathNode02.version = 1;
learningPathNode03.instruction = '';
learningPathNode03.language = Language.English;
learningPathNode03.learningObjectHruid = 'id03';
learningPathNode03.startNode = true;
learningPathNode03.transitions = [transitions03];
learningPathNode03.version = 1;
learningPathNode04.instruction = '';
learningPathNode04.language = Language.English;
learningPathNode04.learningObjectHruid = 'id04';
learningPathNode04.startNode = false;
learningPathNode04.transitions = [transitions04];
learningPathNode04.version = 1;
learningPathNode05.instruction = '';
learningPathNode05.language = Language.English;
learningPathNode05.learningObjectHruid = 'id05';
learningPathNode05.startNode = false;
learningPathNode05.transitions = [transitions05];
learningPathNode05.version = 1;
const nodes01: LearningPathNode[] = [
// LearningPathNode01,
// LearningPathNode02,
];
const learningPath01 = em.create(LearningPath, {
hruid: 'id01',
language: Language.English, language: Language.English,
admins: [],
title: 'repertoire Tool', title: 'repertoire Tool',
description: 'all about Tool', description: 'all about Tool',
image: null, nodes: [
nodes: nodes01, {
}); learningobject_hruid: testLearningObject01.hruid,
language: testLearningObject01.language,
version: testLearningObject01.version,
start_node: true,
created_at: nowString,
updatedAt: nowString,
transitions: [
{
next: {
hruid: testLearningObject02.hruid,
language: testLearningObject02.language,
version: testLearningObject02.version,
},
},
],
},
{
learningobject_hruid: testLearningObject02.hruid,
language: testLearningObject02.language,
version: testLearningObject02.version,
created_at: nowString,
updatedAt: nowString,
transitions: [],
},
],
};
const nodes02: LearningPathNode[] = [ export const testLearningPath02: LearningPathDTO = {
// LearningPathNode03, keywords: 'test',
// LearningPathNode04, target_ages: [16, 17, 18],
// LearningPathNode05, hruid: `${getEnvVar(envVars.UserContentPrefix)}id02`,
];
const learningPath02 = em.create(LearningPath, {
hruid: 'id02',
language: Language.English, language: Language.English,
admins: [],
title: 'repertoire Dire Straits', title: 'repertoire Dire Straits',
description: 'all about Dire Straits', description: 'all about Dire Straits',
image: null, nodes: [
nodes: nodes02, {
}); learningobject_hruid: testLearningObject03.hruid,
language: testLearningObject03.language,
version: testLearningObject03.version,
start_node: true,
created_at: nowString,
updatedAt: nowString,
transitions: [
{
next: {
hruid: testLearningObject04.hruid,
language: testLearningObject04.language,
version: testLearningObject04.version,
},
},
],
},
{
learningobject_hruid: testLearningObject04.hruid,
language: testLearningObject04.language,
version: testLearningObject04.version,
created_at: nowString,
updatedAt: nowString,
transitions: [
{
next: {
hruid: testLearningObject05.hruid,
language: testLearningObject05.language,
version: testLearningObject05.version,
},
},
],
},
{
learningobject_hruid: testLearningObject05.hruid,
language: testLearningObject05.language,
version: testLearningObject05.version,
created_at: nowString,
updatedAt: nowString,
transitions: [],
},
],
};
return [learningPath01, learningPath02]; export const testPartiallyDatabaseAndPartiallyDwengoApiLearningPath: LearningPathDTO = {
} hruid: `${getEnvVar(envVars.UserContentPrefix)}pn_werking`,
title: 'Werken met notebooks',
language: Language.Dutch,
description: 'Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiënt met de notebooks aan de slag?',
keywords: 'Python KIKS Wiskunde STEM AI',
target_ages: [14, 15, 16, 17, 18],
nodes: [
{
learningobject_hruid: testLearningObjectPnNotebooks.hruid,
language: testLearningObjectPnNotebooks.language,
version: testLearningObjectPnNotebooks.version,
start_node: true,
created_at: nowString,
updatedAt: nowString,
transitions: [
{
default: true,
next: {
hruid: 'pn_werkingnotebooks2',
language: Language.Dutch,
version: 3,
},
},
],
},
{
learningobject_hruid: 'pn_werkingnotebooks2',
language: Language.Dutch,
version: 3,
created_at: nowString,
updatedAt: nowString,
transitions: [
{
default: true,
next: {
hruid: 'pn_werkingnotebooks3',
language: Language.Dutch,
version: 3,
},
},
],
},
{
learningobject_hruid: 'pn_werkingnotebooks3',
language: Language.Dutch,
version: 3,
created_at: nowString,
updatedAt: nowString,
transitions: [],
},
],
};
export const testLearningPathWithConditions: LearningPathDTO = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}test_conditions`,
language: Language.English,
title: 'Example learning path with conditional transitions',
description: 'This learning path was made for the purpose of testing conditional transitions',
keywords: 'test',
target_ages: [10, 11, 12, 13, 14, 15, 16, 17, 18],
nodes: [
{
learningobject_hruid: testLearningObjectMultipleChoice.hruid,
language: testLearningObjectMultipleChoice.language,
version: testLearningObjectMultipleChoice.version,
start_node: true,
created_at: nowString,
updatedAt: nowString,
transitions: [
{
// If the answer to the first question was the first one (It's difficult to follow along):
condition: '$[?(@[0] == 0)]',
next: {
//... we let the student do an extra exercise.
hruid: testLearningObject01.hruid,
language: testLearningObject01.language,
version: testLearningObject01.version,
},
},
{
// If the answer to the first question was the second one (I can follow along):
condition: '$[?(@[0] == 1)]',
next: {
//... we let the student right through to the final question.
hruid: testLearningObjectEssayQuestion.hruid,
language: testLearningObjectEssayQuestion.language,
version: testLearningObjectEssayQuestion.version,
},
},
],
},
{
learningobject_hruid: testLearningObject01.hruid,
language: testLearningObject01.language,
version: testLearningObject01.version,
created_at: nowString,
updatedAt: nowString,
transitions: [
{
default: true,
next: {
hruid: testLearningObjectEssayQuestion.hruid,
language: testLearningObjectEssayQuestion.language,
version: testLearningObjectEssayQuestion.version,
},
},
],
},
{
learningobject_hruid: testLearningObjectEssayQuestion.hruid,
language: testLearningObjectEssayQuestion.language,
version: testLearningObjectEssayQuestion.version,
created_at: nowString,
updatedAt: nowString,
transitions: [],
},
],
};

View file

@ -15,7 +15,14 @@ export const TEST_STUDENTS = [
{ username: 'testleerling1', firstName: 'Gerald', lastName: 'Schmittinger' }, { username: 'testleerling1', firstName: 'Gerald', lastName: 'Schmittinger' },
]; ];
let testStudents: Student[];
// 🏗️ Functie die ORM entities maakt uit de data array // 🏗️ Functie die ORM entities maakt uit de data array
export function makeTestStudents(em: EntityManager): Student[] { export function makeTestStudents(em: EntityManager): Student[] {
return TEST_STUDENTS.map((data) => em.create(Student, data)); testStudents = TEST_STUDENTS.map((data) => em.create(Student, data));
return testStudents;
}
export function getTestleerling1(): Student {
return testStudents.find((it) => it.username === 'testleerling1');
} }

View file

@ -2,37 +2,63 @@ import { Teacher } from '../../../src/entities/users/teacher.entity';
import { EntityManager } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/core';
export function makeTestTeachers(em: EntityManager): Teacher[] { export function makeTestTeachers(em: EntityManager): Teacher[] {
const teacher01 = em.create(Teacher, { teacher01 = em.create(Teacher, {
username: 'FooFighters', username: 'FooFighters',
firstName: 'Dave', firstName: 'Dave',
lastName: 'Grohl', lastName: 'Grohl',
}); });
const teacher02 = em.create(Teacher, { teacher02 = em.create(Teacher, {
username: 'LimpBizkit', username: 'LimpBizkit',
firstName: 'Fred', firstName: 'Fred',
lastName: 'Durst', lastName: 'Durst',
}); });
const teacher03 = em.create(Teacher, { teacher03 = em.create(Teacher, {
username: 'Staind', username: 'Staind',
firstName: 'Aaron', firstName: 'Aaron',
lastName: 'Lewis', lastName: 'Lewis',
}); });
// Should not be used, gets deleted in a unit test // Should not be used, gets deleted in a unit test
const teacher04 = em.create(Teacher, { teacher04 = em.create(Teacher, {
username: 'ZesdeMetaal', username: 'ZesdeMetaal',
firstName: 'Wannes', firstName: 'Wannes',
lastName: 'Cappelle', lastName: 'Cappelle',
}); });
// Makes sure when logged in as testleerkracht1, there exists a corresponding user // Makes sure when logged in as testleerkracht1, there exists a corresponding user
const teacher05 = em.create(Teacher, { testleerkracht1 = em.create(Teacher, {
username: 'testleerkracht1', username: 'testleerkracht1',
firstName: 'Bob', firstName: 'Kris',
lastName: 'Dylan', lastName: 'Coolsaet',
}); });
return [teacher01, teacher02, teacher03, teacher04, teacher05]; return [teacher01, teacher02, teacher03, teacher04, testleerkracht1];
}
let teacher01: Teacher;
let teacher02: Teacher;
let teacher03: Teacher;
let teacher04: Teacher;
let testleerkracht1: Teacher;
export function getTeacher01(): Teacher {
return teacher01;
}
export function getTeacher02(): Teacher {
return teacher02;
}
export function getTeacher03(): Teacher {
return teacher03;
}
export function getTeacher04(): Teacher {
return teacher04;
}
export function getTestleerkracht1(): Teacher {
return testleerkracht1;
} }

View file

@ -15,7 +15,7 @@ import { makeTestStudents } from '../tests/test_assets/users/students.testdata.j
import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js'; import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js';
import { getLogger, Logger } from '../src/logging/initalize.js'; import { getLogger, Logger } from '../src/logging/initalize.js';
import { Collection } from '@mikro-orm/core'; import { Collection } from '@mikro-orm/core';
import { Group } from '../dist/entities/assignments/group.entity.js'; import { Group } from '../src/entities/assignments/group.entity';
const logger: Logger = getLogger(); const logger: Logger = getLogger();
@ -34,6 +34,7 @@ export async function seedDatabase(): Promise<void> {
const learningPaths = makeTestLearningPaths(em); const learningPaths = makeTestLearningPaths(em);
const classes = makeTestClasses(em, students, teachers); const classes = makeTestClasses(em, students, teachers);
const assignments = makeTestAssignemnts(em, classes); const assignments = makeTestAssignemnts(em, classes);
const groups = makeTestGroups(em, students, assignments); const groups = makeTestGroups(em, students, assignments);
assignments[0].groups = new Collection<Group>(groups.slice(0, 3)); assignments[0].groups = new Collection<Group>(groups.slice(0, 3));

View file

@ -7,5 +7,10 @@ export interface AssignmentDTO {
description: string; description: string;
learningPath: string; learningPath: string;
language: string; language: string;
groups?: GroupDTO[] | string[][]; // TODO groups: GroupDTO[] | string[][];
}
export interface AssignmentDTOId {
id: number;
within: string;
} }

View file

@ -8,3 +8,9 @@ export interface GroupDTO {
groupNumber: number; groupNumber: number;
members?: string[] | StudentDTO[]; members?: string[] | StudentDTO[];
} }
export interface GroupDTOId {
class: string;
assignment: number;
groupNumber: number;
}

View file

@ -1,14 +1,15 @@
import { Language } from '../util/language'; import { Language } from '../util/language';
export interface Transition { export interface Transition {
default: boolean; default?: boolean;
_id: string; _id?: string;
next: { next: {
_id: string; _id?: string;
hruid: string; hruid: string;
version: number; version: number;
language: string; language: string;
}; };
condition?: string;
} }
export interface LearningObjectIdentifierDTO { export interface LearningObjectIdentifierDTO {
@ -18,7 +19,7 @@ export interface LearningObjectIdentifierDTO {
} }
export interface LearningObjectNode { export interface LearningObjectNode {
_id: string; _id?: string;
learningobject_hruid: string; learningobject_hruid: string;
version: number; version: number;
language: Language; language: Language;
@ -30,20 +31,20 @@ export interface LearningObjectNode {
} }
export interface LearningPath { export interface LearningPath {
_id: string; _id?: string;
language: string; language: string;
hruid: string; hruid: string;
title: string; title: string;
description: string; description: string;
image?: string; // Image might be missing, so it's optional image?: string; // Image might be missing, so it's optional
num_nodes: number; num_nodes?: number;
num_nodes_left: number; num_nodes_left?: number;
nodes: LearningObjectNode[]; nodes: LearningObjectNode[];
keywords: string; keywords: string;
target_ages: number[]; target_ages: number[];
min_age: number; min_age?: number;
max_age: number; max_age?: number;
__order: number; __order?: number;
} }
export interface LearningPathIdentifier { export interface LearningPathIdentifier {
@ -62,8 +63,8 @@ export interface ReturnValue {
} }
export interface LearningObjectMetadata { export interface LearningObjectMetadata {
_id: string; _id?: string;
uuid: string; uuid?: string;
hruid: string; hruid: string;
version: number; version: number;
language: Language; language: Language;
@ -84,7 +85,7 @@ export interface LearningObjectMetadata {
export interface FilteredLearningObject { export interface FilteredLearningObject {
key: string; key: string;
_id: string; _id?: string;
uuid: string; uuid: string;
version: number; version: number;
title: string; title: string;

View file

@ -9,7 +9,7 @@ export interface SubmissionDTO {
submissionNumber?: number; submissionNumber?: number;
submitter: StudentDTO; submitter: StudentDTO;
time?: Date; time?: Date;
group: GroupDTO; group?: GroupDTO;
content: string; content: string;
} }

View file

@ -71,7 +71,7 @@ export default [
'init-declarations': 'off', 'init-declarations': 'off',
'@typescript-eslint/init-declarations': 'off', '@typescript-eslint/init-declarations': 'off',
'max-params': 'off', 'max-params': 'off',
'@typescript-eslint/max-params': ['error', { max: 6 }], '@typescript-eslint/max-params': 'off',
'@typescript-eslint/member-ordering': 'error', '@typescript-eslint/member-ordering': 'error',
'@typescript-eslint/method-signature-style': 'off', // Don't care about TypeScript strict mode. '@typescript-eslint/method-signature-style': 'off', // Don't care about TypeScript strict mode.
'@typescript-eslint/naming-convention': [ '@typescript-eslint/naming-convention': [
@ -87,6 +87,7 @@ export default [
modifiers: ['const'], modifiers: ['const'],
format: ['camelCase', 'UPPER_CASE'], format: ['camelCase', 'UPPER_CASE'],
trailingUnderscore: 'allow', trailingUnderscore: 'allow',
leadingUnderscore: 'allow',
}, },
{ {
// Enforce that private members are prefixed with an underscore // Enforce that private members are prefixed with an underscore

View file

@ -10,12 +10,14 @@
"preview": "vite preview", "preview": "vite preview",
"type-check": "vue-tsc --build", "type-check": "vue-tsc --build",
"format": "prettier --write src/", "format": "prettier --write src/",
"test:e2e": "playwright test",
"format-check": "prettier --check src/", "format-check": "prettier --check src/",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"test:unit": "vitest --run", "pretest:unit": "tsx ../docs/api/generate.ts && npm run build",
"test:e2e": "playwright test" "test:unit": "vitest --run"
}, },
"dependencies": { "dependencies": {
"@dwengo-1/common": "^0.1.1",
"@tanstack/react-query": "^5.69.0", "@tanstack/react-query": "^5.69.0",
"@tanstack/vue-query": "^5.69.0", "@tanstack/vue-query": "^5.69.0",
"@vueuse/core": "^13.1.0", "@vueuse/core": "^13.1.0",

View file

@ -1,11 +1,11 @@
import { BaseController } from "./base-controller"; import { BaseController } from "./base-controller";
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; import type { AssignmentDTO, AssignmentDTOId } from "@dwengo-1/common/interfaces/assignment";
import type { SubmissionsResponse } from "./submissions"; import type { SubmissionsResponse } from "./submissions";
import type { QuestionsResponse } from "./questions"; import type { QuestionsResponse } from "./questions";
import type { GroupsResponse } from "./groups"; import type { GroupsResponse } from "./groups";
export interface AssignmentsResponse { export interface AssignmentsResponse {
assignments: AssignmentDTO[] | string[]; assignments: AssignmentDTO[] | AssignmentDTOId[];
} }
export interface AssignmentResponse { export interface AssignmentResponse {

View file

@ -1,10 +1,10 @@
import { BaseController } from "./base-controller"; import { BaseController } from "./base-controller";
import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; import type { GroupDTO, GroupDTOId } from "@dwengo-1/common/interfaces/group";
import type { SubmissionsResponse } from "./submissions"; import type { SubmissionsResponse } from "./submissions";
import type { QuestionsResponse } from "./questions"; import type { QuestionsResponse } from "./questions";
export interface GroupsResponse { export interface GroupsResponse {
groups: GroupDTO[]; groups: GroupDTO[] | GroupDTOId[];
} }
export interface GroupResponse { export interface GroupResponse {

View file

@ -8,26 +8,24 @@ export class LearningPathController extends BaseController {
constructor() { constructor() {
super("learningPath"); super("learningPath");
} }
async search(query: string, language: string): Promise<LearningPath[]> {
async search(query: string): Promise<LearningPath[]> { const dtos = await this.get<LearningPathDTO[]>("/", { search: query, language });
const dtos = await this.get<LearningPathDTO[]>("/", {search: query});
return dtos.map((dto) => LearningPath.fromDTO(dto)); return dtos.map((dto) => LearningPath.fromDTO(dto));
} }
async getBy( async getBy(
hruid: string, hruid: string,
language: Language, language: Language,
options?: { forGroup?: string; forStudent?: string }, forGroup?: { forGroup: number; assignmentNo: number; classId: string },
): Promise<LearningPath> { ): Promise<LearningPath> {
const dtos = await this.get<LearningPathDTO[]>("/", { const dtos = await this.get<LearningPathDTO[]>("/", {
hruid, hruid,
language, language,
forGroup: options?.forGroup, forGroup: forGroup?.forGroup,
forStudent: options?.forStudent, assignmentNo: forGroup?.assignmentNo,
classId: forGroup?.classId,
}); });
return LearningPath.fromDTO(single(dtos)); return LearningPath.fromDTO(single(dtos));
} }
async getAllByTheme(theme: string): Promise<LearningPath[]> { async getAllByTheme(theme: string): Promise<LearningPath[]> {
const dtos = await this.get<LearningPathDTO[]>("/", { theme }); const dtos = await this.get<LearningPathDTO[]>("/", { theme });
return dtos.map((dto) => LearningPath.fromDTO(dto)); return dtos.map((dto) => LearningPath.fromDTO(dto));

View file

@ -1,5 +1,6 @@
import { BaseController } from "./base-controller"; import { BaseController } from "./base-controller";
import type { SubmissionDTO, SubmissionDTOId } from "@dwengo-1/common/interfaces/submission"; import type { SubmissionDTO, SubmissionDTOId } from "@dwengo-1/common/interfaces/submission";
import type { Language } from "@dwengo-1/common/util/language";
export interface SubmissionsResponse { export interface SubmissionsResponse {
submissions: SubmissionDTO[] | SubmissionDTOId[]; submissions: SubmissionDTO[] | SubmissionDTOId[];
@ -10,16 +11,36 @@ export interface SubmissionResponse {
} }
export class SubmissionController extends BaseController { export class SubmissionController extends BaseController {
constructor(classid: string, assignmentNumber: number, groupNumber: number) { constructor(hruid: string) {
super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}/submissions`); super(`learningObject/${hruid}/submissions`);
} }
async getAll(full = true): Promise<SubmissionsResponse> { async getAll(
return this.get<SubmissionsResponse>(`/`, { full }); language: Language,
version: number,
classId: string,
assignmentId: number,
groupId?: number,
full = true,
): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(`/`, { language, version, classId, assignmentId, groupId, full });
} }
async getByNumber(submissionNumber: number): Promise<SubmissionResponse> { async getByNumber(
return this.get<SubmissionResponse>(`/${submissionNumber}`); language: Language,
version: number,
classId: string,
assignmentId: number,
groupId: number,
submissionNumber: number,
): Promise<SubmissionResponse> {
return this.get<SubmissionResponse>(`/${submissionNumber}`, {
language,
version,
classId,
assignmentId,
groupId,
});
} }
async createSubmission(data: SubmissionDTO): Promise<SubmissionResponse> { async createSubmission(data: SubmissionDTO): Promise<SubmissionResponse> {

View file

@ -1,13 +1,13 @@
{ {
"welcome": "Willkommen", "welcome": "Willkommen",
"student": "schüler", "student": "Schüler",
"teacher": "lehrer", "teacher": "Lehrer",
"assignments": "Aufgaben", "assignments": "Aufgaben",
"classes": "Klasses", "classes": "Klassen",
"discussions": "Diskussionen", "discussions": "Diskussionen",
"login": "einloggen", "login": "einloggen",
"logout": "ausloggen", "logout": "ausloggen",
"cancel": "kündigen", "cancel": "abbrechen",
"logoutVerification": "Sind Sie sicher, dass Sie sich abmelden wollen?", "logoutVerification": "Sind Sie sicher, dass Sie sich abmelden wollen?",
"homeTitle": "Unsere Stärken", "homeTitle": "Unsere Stärken",
"homeIntroduction1": "Wir entwickeln innovative Workshops und Bildungsressourcen, die wir in Zusammenarbeit mit Lehrern und Freiwilligen Schülern auf der ganzen Welt zur Verfügung stellen. Unsere Train-the-Trainer-Sitzungen ermöglichen es ihnen, unsere praktischen Workshops an die Schüler weiterzugeben.", "homeIntroduction1": "Wir entwickeln innovative Workshops und Bildungsressourcen, die wir in Zusammenarbeit mit Lehrern und Freiwilligen Schülern auf der ganzen Welt zur Verfügung stellen. Unsere Train-the-Trainer-Sitzungen ermöglichen es ihnen, unsere praktischen Workshops an die Schüler weiterzugeben.",
@ -23,10 +23,10 @@
"submitCode": "senden", "submitCode": "senden",
"members": "Mitglieder", "members": "Mitglieder",
"themes": "Themen", "themes": "Themen",
"choose-theme": "Wähle ein thema", "choose-theme": "Wählen Sie ein Thema",
"choose-age": "Alter auswählen", "choose-age": "Alter auswählen",
"theme-options": { "theme-options": {
"all": "Alle themen", "all": "Alle Themen",
"culture": "Kultur", "culture": "Kultur",
"electricity-and-mechanics": "Elektrizität und Mechanik", "electricity-and-mechanics": "Elektrizität und Mechanik",
"nature-and-climate": "Natur und Klima", "nature-and-climate": "Natur und Klima",
@ -37,11 +37,11 @@
"algorithms": "Algorithmisches Denken" "algorithms": "Algorithmisches Denken"
}, },
"age-options": { "age-options": {
"all": "Alle altersgruppen", "all": "Alle Altersgruppen",
"primary-school": "Grundschule", "primary-school": "Grundschule",
"lower-secondary": "12-14 jahre alt", "lower-secondary": "12-14 Jahre alt",
"upper-secondary": "14-16 jahre alt", "upper-secondary": "14-16 Jahre alt",
"high-school": "16-18 jahre alt", "high-school": "16-18 Jahre alt",
"older": "18 und älter" "older": "18 und älter"
}, },
"read-more": "Mehr lesen", "read-more": "Mehr lesen",
@ -86,9 +86,20 @@
"accept": "akzeptieren", "accept": "akzeptieren",
"deny": "ablehnen", "deny": "ablehnen",
"sent": "sent", "sent": "sent",
"failed": "gescheitert", "failed": "fehlgeschlagen",
"wrong": "etwas ist schief gelaufen", "wrong": "etwas ist schief gelaufen",
"created": "erstellt", "created": "erstellt",
"submitSolution": "Lösung einreichen",
"submitNewSolution": "Neue Lösung einreichen",
"markAsDone": "Als fertig markieren",
"groupSubmissions": "Einreichungen dieser Gruppe",
"taskCompleted": "Aufgabe erledigt.",
"submittedBy": "Eingereicht von",
"timestamp": "Zeitpunkt",
"loadSubmission": "Einladen",
"noSubmissionsYet": "Noch keine Lösungen eingereicht.",
"viewAsGroup": "Fortschritt ansehen von Gruppe...",
"assignLearningPath": "Als Aufgabe geben"
"group": "Gruppe", "group": "Gruppe",
"description": "Beschreibung", "description": "Beschreibung",
"no-submission": "keine vorlage", "no-submission": "keine vorlage",

View file

@ -89,6 +89,17 @@
"failed": "failed", "failed": "failed",
"wrong": "something went wrong", "wrong": "something went wrong",
"created": "created", "created": "created",
"submitSolution": "Submit solution",
"submitNewSolution": "Submit new solution",
"markAsDone": "Mark as completed",
"groupSubmissions": "This group's submissions",
"taskCompleted": "Task completed.",
"submittedBy": "Submitted by",
"timestamp": "Timestamp",
"loadSubmission": "Load",
"noSubmissionsYet": "No submissions yet.",
"viewAsGroup": "View progress of group...",
"assignLearningPath": "assign",
"group": "Group", "group": "Group",
"description": "Description", "description": "Description",
"no-submission": "no submission", "no-submission": "no submission",

View file

@ -89,6 +89,17 @@
"failed": "échoué", "failed": "échoué",
"wrong": "quelque chose n'a pas fonctionné", "wrong": "quelque chose n'a pas fonctionné",
"created": "créé", "created": "créé",
"submitSolution": "Soumettre la solution",
"submitNewSolution": "Soumettre une nouvelle solution",
"markAsDone": "Marquer comme terminé",
"groupSubmissions": "Soumissions de ce groupe",
"taskCompleted": "Tâche terminée.",
"submittedBy": "Soumis par",
"timestamp": "Horodatage",
"loadSubmission": "Charger",
"noSubmissionsYet": "Pas encore de soumissions.",
"viewAsGroup": "Voir la progression du groupe...",
"assignLearningPath": "donner comme tâche"
"group": "Groupe", "group": "Groupe",
"description": "Description", "description": "Description",
"no-submission": "aucune soumission", "no-submission": "aucune soumission",

Some files were not shown because too many files have changed in this diff Show more