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,53 +11,44 @@ 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) { if (
expected.name = 'expected'; property in IGNORE_PROPERTIES &&
} 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.
for (const property in expected.entity) { typeof expected[property] !== 'function' // Functions obviously are not persisted via the database
if ( ) {
property in IGNORE_PROPERTIES && if (!Object.prototype.hasOwnProperty.call(actual, property)) {
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.
typeof expected.entity[property] !== 'function' // Functions obviously are not persisted via the database
) {
if (!Object.prototype.hasOwnProperty.call(actual.entity, property)) {
throw new AssertionError({
message: `${expected.name} has defined property ${property}, but ${actual.name} is missing it.`,
});
}
if (typeof expected.entity[property] === 'boolean') {
// 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])) {
throw new AssertionError({ throw new AssertionError({
message: `${property} was ${expected.entity[property]} in ${expected.name}, message: `Expected property ${prefixedProperty}, but it is missing.`,
but ${actual.entity[property]} (${Boolean(expected.entity[property])}) in ${actual.name}`,
}); });
} }
} else if (typeof expected.entity[property] !== typeof actual.entity[property]) { if (typeof expected[property] === 'boolean') {
throw new AssertionError({ // Sometimes, booleans get represented by numbers 0 and 1 in the objects actual from the database.
message: `${property} has type ${typeof expected.entity[property]} in ${expected.name}, but type ${typeof actual.entity[property]} in ${actual.name}.`, if (Boolean(expected[property]) !== Boolean(actual[property])) {
}); throw new AssertionError({
} else if (typeof expected.entity[property] === 'object') { message: `Expected ${prefixedProperty} to be ${expected[property]},
expectToBeCorrectEntity( but was ${actual[property]} (${Boolean(expected[property])}).`,
{ });
name: actual.name + '.' + property,
entity: actual.entity[property] as object,
},
{
name: expected.name + '.' + property,
entity: expected.entity[property] as object,
} }
); } else if (typeof expected[property] !== typeof actual[property]) {
} else {
if (expected.entity[property] !== actual.entity[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 have type ${typeof expected[property]},` +
`but had type ${typeof actual[property]}.`,
}); });
} else if (typeof expected[property] === 'object') {
expectToBeCorrectEntity(actual[property] as object, expected[property] as object, property);
} else {
if (expected[property] !== actual[property]) {
throw new AssertionError({
message: `${prefixedProperty} was expected to be ${expected[property]}, ` + `but was ${actual[property]}.`,
});
}
} }
} }
} }
@ -67,9 +58,9 @@ export function expectToBeCorrectEntity<T extends object>(actual: { entity: T; n
/** /**
* 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 },
])
);
for (const node of learningPath.nodes) {
const nodeKey = {
learningObjectHruid: node.learningobject_hruid,
language: node.language,
version: node.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(correspondingNode).toBeTruthy();
expect(new Set(node.transitions.map((it) => it.next.version))).toEqual(new Set(expectedNode.transitions.map((it) => it.next.version))); expect(Boolean(correspondingNode!.start_node)).toEqual(Boolean(node.start_node));
for (const transition of node.transitions) {
const correspondingTransition = correspondingNode!.transitions.find(
(it) =>
it.next.hruid === transition.next.hruid &&
it.next.language === transition.next.language &&
it.next.version === transition.next.version
);
expect(correspondingTransition).toBeTruthy();
}
} }
} }
/**
* 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,135 +1,261 @@
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);
language: Language.English, const learningObject03 = em.create(LearningObject, testLearningObject03);
version: 1, const learningObject04 = em.create(LearningObject, testLearningObject04);
admins: [], const learningObject05 = em.create(LearningObject, testLearningObject05);
title: 'Undertow',
description: 'debute',
contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: '',
license: '',
estimatedTime: 45,
returnValue: returnValue,
available: true,
contentLocation: '',
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"),
});
const learningObject02 = em.create(LearningObject, { const learningObjectMultipleChoice = em.create(LearningObject, testLearningObjectMultipleChoice);
hruid: 'id02', const learningObjectEssayQuestion = em.create(LearningObject, testLearningObjectEssayQuestion);
language: Language.English,
version: 1,
admins: [],
title: 'Aenema',
description: 'second album',
contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: '',
license: '',
estimatedTime: 80,
returnValue: returnValue,
available: true,
contentLocation: '',
attachments: [],
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"
),
});
const learningObject03 = em.create(LearningObject, { const learningObjectPnNotebooks = em.create(LearningObject, testLearningObjectPnNotebooks);
hruid: 'id03',
language: Language.English,
version: 1,
admins: [],
title: 'love over gold',
description: 'third album',
contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: '',
license: '',
estimatedTime: 55,
returnValue: returnValue,
available: true,
contentLocation: '',
attachments: [],
content: Buffer.from(
'he wrote me a prescription, he said you are depressed, \
but I am glad you came to see me to get this off your chest, \
come back and see me later next patient please \
send in another victim of industrial disease'
),
});
const learningObject04 = em.create(LearningObject, { return [
hruid: 'id04', learningObject01,
language: Language.English, learningObject02,
version: 1, learningObject03,
admins: [], learningObject04,
title: 'making movies', learningObject05,
description: 'fifth album', learningObjectMultipleChoice,
contentType: DwengoContentType.TEXT_MARKDOWN, learningObjectEssayQuestion,
keywords: [], learningObjectPnNotebooks,
teacherExclusive: false, ];
skosConcepts: [],
educationalGoals: [],
copyright: '',
license: '',
estimatedTime: 55,
returnValue: returnValue,
available: true,
contentLocation: '',
attachments: [],
content: Buffer.from(
'I put my hand upon the lever \
Said let it rock and let it roll \
I had the one-arm bandit fever \
There was an arrow through my heart and my soul'
),
});
const learningObject05 = em.create(LearningObject, {
hruid: 'id05',
language: Language.English,
version: 1,
admins: [],
title: 'on every street',
description: 'sixth album',
contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: '',
license: '',
estimatedTime: 55,
returnValue: returnValue,
available: true,
contentLocation: '',
attachments: [],
content: Buffer.from('calling Elvis, is anybody home, calling elvis, I am here all alone'),
});
return [learningObject01, learningObject02, learningObject03, learningObject04, learningObject05];
} }
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,
version: 1,
admins: [],
title: 'Undertow',
description: 'debute',
contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [],
uuid: v4(),
targetAges: [16, 17, 18],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: '',
license: '',
estimatedTime: 45,
returnValue: createReturnValue(),
available: true,
contentLocation: '',
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"),
};
export const testLearningObject02: RequiredEntityData<LearningObject> = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}id02`,
language: Language.English,
version: 1,
admins: [],
title: 'Aenema',
description: 'second album',
contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: '',
license: '',
estimatedTime: 80,
returnValue: createReturnValue(),
available: true,
contentLocation: '',
attachments: [],
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"
),
};
export const testLearningObject03: RequiredEntityData<LearningObject> = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}id03`,
language: Language.English,
version: 1,
admins: [],
title: 'love over gold',
description: 'third album',
contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: '',
license: '',
estimatedTime: 55,
returnValue: createReturnValue(),
available: true,
contentLocation: '',
attachments: [],
content: Buffer.from(
'he wrote me a prescription, he said you are depressed, \
but I am glad you came to see me to get this off your chest, \
come back and see me later next patient please \
send in another victim of industrial disease'
),
};
export const testLearningObject04: RequiredEntityData<LearningObject> = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}id04`,
language: Language.English,
version: 1,
admins: [],
title: 'making movies',
description: 'fifth album',
contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: '',
license: '',
estimatedTime: 55,
returnValue: createReturnValue(),
available: true,
contentLocation: '',
attachments: [],
content: Buffer.from(
'I put my hand upon the lever \
Said let it rock and let it roll \
I had the one-arm bandit fever \
There was an arrow through my heart and my soul'
),
};
export const testLearningObject05: RequiredEntityData<LearningObject> = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}id05`,
language: Language.English,
version: 1,
admins: [],
title: 'on every street',
description: 'sixth album',
contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
copyright: '',
license: '',
estimatedTime: 55,
returnValue: createReturnValue(),
available: true,
contentLocation: '',
attachments: [],
content: Buffer.from('calling Elvis, is anybody home, calling elvis, I am here all alone'),
};
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';
transitions02.next = learningPathNode02;
transitions03.condition = 'true';
transitions03.next = learningPathNode04;
transitions04.condition = 'true';
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,
admins: [],
title: 'repertoire Tool',
description: 'all about Tool',
image: null,
nodes: nodes01,
});
const nodes02: LearningPathNode[] = [
// LearningPathNode03,
// LearningPathNode04,
// LearningPathNode05,
];
const learningPath02 = em.create(LearningPath, {
hruid: 'id02',
language: Language.English,
admins: [],
title: 'repertoire Dire Straits',
description: 'all about Dire Straits',
image: null,
nodes: nodes02,
});
return [learningPath01, learningPath02];
} }
const nowString = new Date().toString();
export const testLearningPath01: LearningPathDTO = {
keywords: 'test',
target_ages: [16, 17, 18],
hruid: `${getEnvVar(envVars.UserContentPrefix)}id01`,
language: Language.English,
title: 'repertoire Tool',
description: 'all about Tool',
nodes: [
{
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: [],
},
],
};
export const testLearningPath02: LearningPathDTO = {
keywords: 'test',
target_ages: [16, 17, 18],
hruid: `${getEnvVar(envVars.UserContentPrefix)}id02`,
language: Language.English,
title: 'repertoire Dire Straits',
description: 'all about Dire Straits',
nodes: [
{
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: [],
},
],
};
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

@ -1,35 +1,33 @@
import {BaseController} from "@/controllers/base-controller.ts"; import { BaseController } from "@/controllers/base-controller.ts";
import {LearningPath} from "@/data-objects/learning-paths/learning-path.ts"; import { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
import type {Language} from "@/data-objects/language.ts"; import type { Language } from "@/data-objects/language.ts";
import {single} from "@/utils/response-assertions.ts"; import { single } from "@/utils/response-assertions.ts";
import type {LearningPathDTO} from "@/data-objects/learning-paths/learning-path-dto.ts"; import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts";
export class LearningPathController extends BaseController { 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