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:
commit
a421b1996a
123 changed files with 2428 additions and 2658 deletions
|
@ -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",
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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' })
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
12
backend/src/exceptions/server-error-exception.ts
Normal file
12
backend/src/exceptions/server-error-exception.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 || [],
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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++;
|
||||||
|
|
|
@ -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++;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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[]>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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[]> {
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
12
backend/src/util/base64-buffer-conversion.ts
Normal file
12
backend/src/util/base64-buffer-conversion.ts
Normal 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;
|
||||||
|
}
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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(),
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -1,2 +0,0 @@
|
||||||
::MC basic::
|
|
||||||
How are you? {}
|
|
|
@ -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>
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -1,3 +0,0 @@
|
||||||
interface LearningPathExample {
|
|
||||||
createLearningPath: () => LearningPath;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
10
backend/tests/test-utils/get-html-rendering.ts
Normal file
10
backend/tests/test-utils/get-html-rendering.ts
Normal 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();
|
||||||
|
}
|
|
@ -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}`));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
::Reflection::
|
||||||
|
Reflect on this learning path. What have you learned today? {}
|
|
@ -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>
|
|
@ -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!
|
||||||
}
|
}
|
|
@ -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>
|
|
@ -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: '[]',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -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: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue