fix(backend): Foute entity-structuur van leerpaden verbeterd.

Ook testen geschreven voor LearningPathRepository en LearningObjectRepository.
This commit is contained in:
Gerald Schmittinger 2025-03-09 08:50:39 +01:00
parent 4d999c78ba
commit 1417907933
24 changed files with 474 additions and 64 deletions

View file

@ -0,0 +1,71 @@
import {beforeAll, describe, it, expect} from "vitest";
import {LearningObjectRepository} from "../../../src/data/content/learning-object-repository";
import {setupTestApp} from "../../setup-tests";
import {getLearningObjectRepository} from "../../../src/data/repositories";
import example from "../../test-assets/learning-objects/pn_werkingnotebooks/pn-werkingnotebooks-example.js"
import {LearningObject} from "../../../src/entities/content/learning-object.entity";
import {expectToBeCorrectEntity} from "../../test-utils/expect-to-be-correct-entity";
describe("LearningObjectRepository", () => {
let learningObjectRepository: LearningObjectRepository;
let exampleLearningObject: LearningObject;
beforeAll(async () => {
await setupTestApp();
learningObjectRepository = getLearningObjectRepository();
});
it("should be able to add a learning object to it without an error", async () => {
exampleLearningObject = example.createLearningObject();
await learningObjectRepository.insert(exampleLearningObject);
});
it("should return the learning object when queried by id", async () => {
const result = await learningObjectRepository.findByIdentifier({
hruid: exampleLearningObject.hruid,
language: exampleLearningObject.language,
version: exampleLearningObject.version
});
expect(result).toBeInstanceOf(LearningObject);
console.log(result);
expectToBeCorrectEntity({
name: "actual",
entity: result!
}, {
name: "expected",
entity: exampleLearningObject
});
});
it("should return null when non-existing version is queried", async () => {
const result = await learningObjectRepository.findByIdentifier({
hruid: exampleLearningObject.hruid,
language: exampleLearningObject.language,
version: 100
});
expect(result).toBe(null);
});
let newerExample: LearningObject;
it("should allow a learning object with the same id except a different version to be added", async () => {
newerExample = example.createLearningObject();
newerExample.version = 10;
newerExample.title += " (nieuw)";
await learningObjectRepository.save(newerExample);
});
it("should return the newest version of the learning object when queried by only hruid and language", async () => {
const result = await learningObjectRepository.findLatestByHruidAndLanguage(newerExample.hruid, newerExample.language);
expect(result).toBeInstanceOf(LearningObject);
expect(result?.version).toBe(10);
expect(result?.title).toContain("(nieuw)");
});
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);
expect(result).toBe(null);
});
});

View file

@ -0,0 +1,78 @@
import {beforeAll, describe, expect, it} from "vitest";
import {setupTestApp} from "../../setup-tests";
import {getLearningPathRepository} from "../../../src/data/repositories";
import {LearningPathRepository} from "../../../src/data/content/learning-path-repository";
import example from "../../test-assets/learning-paths/pn-werking-example";
import {LearningPath} from "../../../src/entities/content/learning-path.entity";
import {expectToBeCorrectEntity} from "../../test-utils/expect-to-be-correct-entity";
import {Language} from "../../../src/entities/content/language";
function expectToHaveFoundPrecisely(expected: LearningPath, result: LearningPath[]): void {
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", () => {
let learningPathRepo: LearningPathRepository;
beforeAll(async () => {
await setupTestApp();
learningPathRepo = getLearningPathRepository();
});
let examplePath: LearningPath;
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);
expectToBeCorrectEntity({ entity: result! }, { entity: examplePath });
});
it("should return null to a query on a non-existing hruid or language", async () => {
const result = await learningPathRepo.findByHruidAndLanguage("not_existing_hruid", examplePath.language);
expect(result).toBe(null);
});
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
);
expectToHaveFoundPrecisely(examplePath, result);
});
it("should return the learning path when we search for a search term occurring in its description", async () => {
const result = await learningPathRepo.findByQueryStringAndLanguage(
examplePath.description.slice(8, 15),
examplePath.language
);
expectToHaveFoundPrecisely(examplePath, result);
});
it("should return null when we search for something not occurring in its title or description", async () => {
const result = await learningPathRepo.findByQueryStringAndLanguage(
"something not occurring in the path",
examplePath.language
);
expectToHaveFoundNothing(result);
});
it("should return null when we search for something occurring in its title, but another language", async () => {
const result = await learningPathRepo.findByQueryStringAndLanguage(
examplePath.description.slice(1, 3),
Language.Kalaallisut
);
expectToHaveFoundNothing(result);
});
});

View file

@ -0,0 +1,7 @@
import {LearningObject} from "../../../src/entities/content/learning-object.entity";
import {Attachment} from "../../../src/entities/content/attachment.entity";
type LearningObjectExample = {
createLearningObject: () => LearningObject,
createAttachment: {[key: string]: (owner: LearningObject) => Attachment}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,25 @@
# Werken met notebooks
Het lesmateriaal van 'Python in wiskunde en STEM' wordt aangeboden in de vorm van interactieve **_notebooks_**. Notebooks zijn _digitale documenten_ die zowel uitvoerbare code bevatten als tekst, afbeeldingen, video, hyperlinks ...
_Nieuwe begrippen_ worden aangebracht via tekstuele uitleg, video en afbeeldingen.
Er zijn uitgewerkte *voorbeelden* met daarnaast ook kleine en grote *opdrachten*. In deze opdrachten zal je aangereikte code kunnen uitvoeren, maar ook zelf code opstellen.
De code die in de notebooks gebruikt wordt, is Python versie 3. We kozen voor Python omdat dit een heel toegankelijke programmeertaal is, die vaak ook intuïtief is.
Python is bovendien bezig aan een opmars en wordt gebruikt door bedrijven, zoals Google, NASA, Netflix, Uber, AstraZeneca, Barco, Instagram en YouTube.
We kozen voor notebooks omdat daar enkele belangrijke voordelen aan verbonden zijn: leerkrachten moeten geen geavanceerde installaties doen om de notebooks te gebruiken, leerkrachten kunnen verschillende soorten van lesinhouden aanbieden via één platform, de notebooks zijn interactief, leerlingen bouwen de oplossing van een probleem stap voor stap op in de notebook waardoor dat proces zichtbaar is voor de leerkracht ([Jeroen Van der Hooft, 2023](https://libstore.ugent.be/fulltxt/RUG01/003/151/437/RUG01-003151437_2023_0001_AC.pdf)).
---
Klik je op onderstaande knop 'Open notebooks', dan word je doorgestuurd naar een andere website waar jouw persoonlijke notebooks ingeladen worden. (Dit kan even duren.)
Links op het scherm vind je er twee bestanden met extensie _.ipynb_.
Dit zijn de twee notebooks waarin je resp. een overzicht krijgt van de opbouw en mogelijkheden en hoe je er mee aan de slag kan. Dubbelklik op de bestandsnaam om een notebook te openen.
Je ziet er ook een map *images* met de afbeeldingen die in de notebooks getoond worden.
In deze eerste twee notebooks leer je hoe de notebooks zijn opgevat en hoe je ermee aan de slag kan.
Na het doorlopen van beide notebooks heb je een goed idee van hoe onze Python notebooks zijn opgevat.
[![](Knop.png "Knop")](https://kiks.ilabt.imec.be/hub/tmplogin?id=0101 "Notebooks Werking")

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -0,0 +1,74 @@
import {LearningObjectExample} from "../learning-object-example";
import {Language} from "../../../../src/entities/content/language";
import {DwengoContentType} from "../../../../src/services/learning-objects/processing/content-type";
import {loadTestAsset} from "../../../test-utils/load-test-asset";
import {EducationalGoal, LearningObject, ReturnValue} from "../../../../src/entities/content/learning-object.entity";
import {Attachment} from "../../../../src/entities/content/attachment.entity";
import {EnvVars, getEnvVar} from "../../../../src/util/envvars";
const ASSETS_PREFIX = "learning-objects/pn_werkingnotebooks/";
const example: LearningObjectExample = {
createLearningObject: ()=> {
let 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"]
let educationalGoal1 = new EducationalGoal();
educationalGoal1.source = "Source";
educationalGoal1.id = "id";
let 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;
let 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}/dwengo.png`);
return learningObject
},
createAttachment: {
dwengoLogo: (learningObject) => {
let 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) => {
let att = new Attachment();
att.learningObject = learningObject;
att.name = "Knop.png";
att.mimeType = "image/png";
att.content = loadTestAsset(`${ASSETS_PREFIX}/Knop.png`)
return att;
}
}
}
export default example;

View file

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

View file

@ -0,0 +1,24 @@
import {Language} from "../../../src/entities/content/language";
import {LearningPathTransition} from "../../../src/entities/content/learning-path-transition.entity";
import {LearningPathNode} from "../../../src/entities/content/learning-path-node.entity";
export function createLearningPathTransition(condition: string | null, to: LearningPathNode) {
let trans = new LearningPathTransition();
trans.condition = condition || "true";
trans.next = to;
return trans;
}
export function createLearningPathNode(
learningObjectHruid: string,
version: number,
language: Language,
startNode: boolean
) {
let node = new LearningPathNode();
node.learningObjectHruid = learningObjectHruid;
node.version = version;
node.language = language;
node.startNode = startNode;
return node;
}

View file

@ -0,0 +1,29 @@
import {LearningPath, LearningPathNode} from "../../../src/entities/content/learning-path.entity";
import {Language} from "../../../src/entities/content/language";
import {EnvVars, getEnvVar} from "../../../src/util/envvars";
import {createLearningPathNode, createLearningPathTransition} from "./learning-path-utils";
function createNodes(): LearningPathNode[] {
let nodes = [
createLearningPathNode("u_pn_werkingnotebooks", 3, Language.Dutch, true),
createLearningPathNode("pn_werkingnotebooks2", 3, Language.Dutch, false),
createLearningPathNode("pn_werkingnotebooks3", 3, Language.Dutch, false),
];
nodes[0].transitions.push(createLearningPathTransition("true", nodes[1]));
nodes[1].transitions.push(createLearningPathTransition("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();
return path;
}
}
export default example;

View file

@ -0,0 +1,62 @@
import {AssertionError} from "node:assert";
// Ignored properties because they belang for example to the class, not to the entity itself.
const IGNORE_PROPERTIES = ["parent"];
/**
* 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 expected The (previously added) entity we would expect to retrieve
*/
export function expectToBeCorrectEntity<T extends object>(
actual: {entity: T, name?: string},
expected: {entity: T, name?: string}
): void {
if (!actual.name) {
actual.name = "actual";
}
if (!expected.name) {
expected.name = "expected";
}
for (let property in expected.entity) {
if (
property !in IGNORE_PROPERTIES
&& expected.entity[property] !== undefined // If we don't expect a certain value for a property, we assume it can be filled in by the database however it wants.
&& typeof expected.entity[property] !== "function" // Functions obviously are not persisted via the database
) {
if (!actual.entity.hasOwnProperty(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 (!!expected.entity[property] !== !!actual.entity[property]) {
throw new AssertionError({
message: `${property} was ${expected.entity[property]} in ${expected.name},
but ${actual.entity[property]} (${!!expected.entity[property]}) in ${actual.name}`
});
}
} else if (typeof expected.entity[property] !== typeof actual.entity[property]) {
throw new AssertionError({
message: `${property} has type ${typeof expected.entity[property]} in ${expected.name}, but type ${typeof actual.entity[property]} in ${actual.name}.`
});
} else if (typeof expected.entity[property] === "object") {
expectToBeCorrectEntity(
{
name: actual.name + "." + property,
entity: actual.entity[property] as object
}, {
name: expected.name + "." + property,
entity: expected.entity[property] as object
}
);
} else {
if (expected.entity[property] !== actual.entity[property]) {
throw new AssertionError({
message: `${property} was ${expected.entity[property]} in ${expected.name}, but ${actual.entity[property]} in ${actual.name}`
});
}
}
}
}
}

View file

@ -0,0 +1,10 @@
import fs from "fs";
import path from "node:path";
/**
* Load the asset at the given path.
* @param relPath Path of the asset relative to the test-assets folder.
*/
export function loadTestAsset(relPath: string): Buffer {
return fs.readFileSync(path.resolve(__dirname, `../test-assets/${relPath}`));
}