diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index 331ff8e2..d69ae075 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -24,5 +24,4 @@ export class LearningObjectRepository extends DwengoEntityRepository { public findByHruidAndLanguage( @@ -17,7 +17,7 @@ export class LearningPathRepository extends DwengoEntityRepository * @param query The query string we want to seach for in the title or description. * @param language The language of the learning paths we want to find. */ - public findByQueryStringAndLanguage(query: string, language: Language): Promise { + public async findByQueryStringAndLanguage(query: string, language: Language): Promise { return this.findAll({ where: { language: language, diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index a92229f0..c38035ca 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -45,7 +45,7 @@ export class LearningObject { keywords: string[] = []; @Property({ type: 'array', nullable: true }) - targetAges?: number[]; + targetAges?: number[] = []; @Property({ type: 'bool' }) teacherExclusive: boolean = false; diff --git a/backend/src/entities/content/learning-path-node.entity.ts b/backend/src/entities/content/learning-path-node.entity.ts new file mode 100644 index 00000000..e15b033a --- /dev/null +++ b/backend/src/entities/content/learning-path-node.entity.ts @@ -0,0 +1,37 @@ +import {Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property} from "@mikro-orm/core"; +import {Language} from "./language"; +import {LearningPath} from "./learning-path.entity"; +import {LearningPathTransition} from "./learning-path-transition.entity"; + +@Entity() +export class LearningPathNode { + @ManyToOne({ entity: () => LearningPath, primary: true }) + learningPath!: LearningPath; + + @PrimaryKey({ type: "numeric", autoincrement: true }) + nodeNumber!: number; + + @Property({ type: 'string' }) + learningObjectHruid!: string; + + @Enum({ items: () => Language }) + language!: Language; + + @Property({ type: 'number' }) + version!: number; + + @Property({ type: 'text', nullable: true }) + instruction?: string; + + @Property({ type: 'bool' }) + startNode!: boolean; + + @OneToMany({ entity: () => LearningPathTransition, mappedBy: "node" }) + transitions: LearningPathTransition[] = []; + + @Property({ length: 3 }) + createdAt: Date = new Date(); + + @Property({ length: 3, onUpdate: () => new Date() }) + updatedAt: Date = new Date(); +} diff --git a/backend/src/entities/content/learning-path-transition.entity.ts b/backend/src/entities/content/learning-path-transition.entity.ts new file mode 100644 index 00000000..6122b758 --- /dev/null +++ b/backend/src/entities/content/learning-path-transition.entity.ts @@ -0,0 +1,17 @@ +import {Entity, ManyToOne, PrimaryKey, Property} from "@mikro-orm/core"; +import {LearningPathNode} from "./learning-path-node.entity"; + +@Entity() +export class LearningPathTransition { + @ManyToOne({entity: () => LearningPathNode }) + node!: LearningPathNode; + + @PrimaryKey({ type: 'numeric' }) + transitionNumber!: number; + + @Property({ type: 'string' }) + condition!: string; + + @ManyToOne({ entity: () => LearningPathNode }) + next!: LearningPathNode; +} diff --git a/backend/src/entities/content/learning-path.entity.ts b/backend/src/entities/content/learning-path.entity.ts index 85406f6e..3bb839a0 100644 --- a/backend/src/entities/content/learning-path.entity.ts +++ b/backend/src/entities/content/learning-path.entity.ts @@ -1,16 +1,14 @@ import { - Embeddable, - Embedded, Entity, Enum, - ManyToMany, - OneToOne, + ManyToMany, OneToMany, PrimaryKey, Property, } from '@mikro-orm/core'; import { Language } from './language.js'; import { Teacher } from '../users/teacher.entity.js'; import {LearningPathRepository} from "../../data/content/learning-path-repository"; +import {LearningPathNode} from "./learning-path-node.entity"; @Entity({repository: () => LearningPathRepository}) export class LearningPath { @@ -29,45 +27,9 @@ export class LearningPath { @Property({ type: 'text' }) description!: string; - @Property({ type: 'blob' }) - image!: string; + @Property({ type: 'blob', nullable: true }) + image: Buffer | null = null; - @Embedded({ entity: () => LearningPathNode, array: true }) + @OneToMany({ entity: () => LearningPathNode, mappedBy: "learningPath" }) nodes: LearningPathNode[] = []; } - -@Embeddable() -export class LearningPathNode { - @Property({ type: 'string' }) - learningObjectHruid!: string; - - @Enum({ items: () => Language }) - language!: Language; - - @Property({ type: 'number' }) - version!: number; - - @Property({ type: 'longtext' }) - instruction!: string; - - @Property({ type: 'bool' }) - startNode!: boolean; - - @Embedded({ entity: () => LearningPathTransition, array: true }) - transitions!: LearningPathTransition[]; - - @Property({ length: 3 }) - createdAt: Date = new Date(); - - @Property({ length: 3, onUpdate: () => new Date() }) - updatedAt: Date = new Date(); -} - -@Embeddable() -export class LearningPathTransition { - @Property({ type: 'string' }) - condition!: string; - - @OneToOne({ entity: () => LearningPathNode }) - next!: LearningPathNode; -} diff --git a/backend/src/services/learning-objects/attachment-service.ts b/backend/src/services/learning-objects/attachment-service.ts index f783663a..2285ddfe 100644 --- a/backend/src/services/learning-objects/attachment-service.ts +++ b/backend/src/services/learning-objects/attachment-service.ts @@ -2,10 +2,10 @@ import {getAttachmentRepository} from "../../data/repositories"; import {Attachment} from "../../entities/content/attachment.entity"; import {LearningObjectIdentifier} from "../../interfaces/learning-content"; -const attachmentRepo = getAttachmentRepository(); - const attachmentService = { getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise { + const attachmentRepo = getAttachmentRepository(); + if (learningObjectId.version) { return attachmentRepo.findByLearningObjectIdAndName({ hruid: learningObjectId.hruid, diff --git a/backend/src/services/learning-objects/database-learning-object-provider.ts b/backend/src/services/learning-objects/database-learning-object-provider.ts index d5d42a4c..46fc23fe 100644 --- a/backend/src/services/learning-objects/database-learning-object-provider.ts +++ b/backend/src/services/learning-objects/database-learning-object-provider.ts @@ -11,8 +11,6 @@ import {getUrlStringForLearningObject} from "../../util/links"; import processingService from "./processing/processing-service"; import {NotFoundError} from "@mikro-orm/core"; -const learningObjectRepo = getLearningObjectRepository(); -const learningPathRepo = getLearningPathRepository(); function convertLearningObject(learningObject: LearningObject | null): FilteredLearningObject | null { if (!learningObject) { @@ -45,6 +43,8 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL } function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise { + const learningObjectRepo = getLearningObjectRepository(); + return learningObjectRepo.findLatestByHruidAndLanguage( id.hruid, id.language as Language ); @@ -66,6 +66,8 @@ const databaseLearningObjectProvider: LearningObjectProvider = { * Obtain a HTML-rendering of the learning object with the given identifier (as a string). */ async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + const learningObjectRepo = getLearningObjectRepository(); + const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage( id.hruid, id.language as Language ); @@ -82,6 +84,8 @@ const databaseLearningObjectProvider: LearningObjectProvider = { * Fetch the HRUIDs of all learning objects on this path. */ async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { + const learningPathRepo = getLearningPathRepository(); + const learningPath = await learningPathRepo.findByHruidAndLanguage(id.hruid, id.language); if (!learningPath) { throw new NotFoundError("The learning path with the given ID could not be found."); @@ -93,6 +97,8 @@ const databaseLearningObjectProvider: LearningObjectProvider = { * Fetch the full metadata of all learning objects on this path. */ async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { + const learningPathRepo = getLearningPathRepository(); + const learningPath = await learningPathRepo.findByHruidAndLanguage(id.hruid, id.language); if (!learningPath) { throw new NotFoundError("The learning path with the given ID could not be found."); diff --git a/backend/src/services/learning-objects/processing/markdown/learning-object-markdown-renderer.ts b/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts similarity index 92% rename from backend/src/services/learning-objects/processing/markdown/learning-object-markdown-renderer.ts rename to backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts index 3a0366af..2dbe51d0 100644 --- a/backend/src/services/learning-objects/processing/markdown/learning-object-markdown-renderer.ts +++ b/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts @@ -1,15 +1,20 @@ +/** + * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/markdown/learing_object_markdown_renderer.js [sic!] + */ import PdfProcessor from "../pdf/pdf-processor.js"; import AudioProcessor from "../audio/audio-processor.js"; import ExternProcessor from "../extern/extern-processor.js"; import InlineImageProcessor from "../image/inline-image-processor.js"; -import {RendererObject, Tokens} from "marked"; +import * as marked from "marked"; import {getUrlStringForLearningObjectHTML, isValidHttpUrl} from "../../../../util/links"; import {ProcessingError} from "../processing-error"; import {LearningObjectIdentifier} from "../../../../interfaces/learning-content"; import {Language} from "../../../../entities/content/language"; -import Image = Tokens.Image; -import Heading = Tokens.Heading; -import Link = Tokens.Link; + +import Image = marked.Tokens.Image; +import Heading = marked.Tokens.Heading; +import Link = marked.Tokens.Link; +import RendererObject = marked.RendererObject; const prefixes = { learningObject: '@learning-object', diff --git a/backend/src/services/learning-objects/processing/markdown/markdown-processor.ts b/backend/src/services/learning-objects/processing/markdown/markdown-processor.ts index 47a185dc..b1a49b6a 100644 --- a/backend/src/services/learning-objects/processing/markdown/markdown-processor.ts +++ b/backend/src/services/learning-objects/processing/markdown/markdown-processor.ts @@ -1,6 +1,5 @@ /** * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/markdown/markdown_processor.js - * and https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/markdown/learing_object_markdown_renderer.js [sic!] */ import {marked} from 'marked' @@ -8,7 +7,7 @@ import Processor from '../processor.js'; import InlineImageProcessor from '../image/inline-image-processor.js'; import {DwengoContentType} from "../content-type"; import {ProcessingError} from "../processing-error"; -import dwengoMarkedRenderer from "./learning-object-markdown-renderer"; +import dwengoMarkedRenderer from "./dwengo-marked-renderer"; class MarkdownProcessor extends Processor { constructor() { diff --git a/backend/src/services/learning-objects/processing/processing-service.ts b/backend/src/services/learning-objects/processing/processing-service.ts index 1e2fc3c9..a9885718 100644 --- a/backend/src/services/learning-objects/processing/processing-service.ts +++ b/backend/src/services/learning-objects/processing/processing-service.ts @@ -35,9 +35,9 @@ class ProcessingService { new GiftProcessor() ]; - processors.forEach(processor => { - this.processors.set(processor.contentType, processor); - }); + this.processors = new Map( + processors.map(processor => [processor.contentType, processor]) + ) } /** diff --git a/backend/src/services/learning-paths/database-learning-path-provider.ts b/backend/src/services/learning-paths/database-learning-path-provider.ts index c3ed6863..879996e4 100644 --- a/backend/src/services/learning-paths/database-learning-path-provider.ts +++ b/backend/src/services/learning-paths/database-learning-path-provider.ts @@ -15,8 +15,6 @@ import {getLearningPathRepository} from "../../data/repositories"; import {Language} from "../../entities/content/language"; import learningObjectService from "../learning-objects/learning-object-service"; -const learningPathRepo = getLearningPathRepository(); - /** * Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its * corresponding learning object. @@ -138,6 +136,8 @@ const databaseLearningPathProvider: LearningPathProvider = { * Fetch the learning paths with the given hruids from the database. */ async fetchLearningPaths(hruids: string[], language: Language, source: string): Promise { + const learningPathRepo = getLearningPathRepository(); + const learningPaths = await Promise.all( hruids.map(hruid => learningPathRepo.findByHruidAndLanguage(hruid, language)) ); @@ -158,6 +158,8 @@ const databaseLearningPathProvider: LearningPathProvider = { * Search learning paths in the database using the given search string. */ async searchLearningPaths(query: string, language: Language): Promise { + const learningPathRepo = getLearningPathRepository(); + const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language); return await Promise.all( searchResults.map((result, index) => diff --git a/backend/tests/data/content/learning-object-repository.test.ts b/backend/tests/data/content/learning-object-repository.test.ts new file mode 100644 index 00000000..366e5a81 --- /dev/null +++ b/backend/tests/data/content/learning-object-repository.test.ts @@ -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); + }); + +}); diff --git a/backend/tests/data/content/learning-path-repository.test.ts b/backend/tests/data/content/learning-path-repository.test.ts new file mode 100644 index 00000000..5e946dfd --- /dev/null +++ b/backend/tests/data/content/learning-path-repository.test.ts @@ -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); + }); +}); diff --git a/backend/tests/test-assets/learning-objects/learning-object-example.d.ts b/backend/tests/test-assets/learning-objects/learning-object-example.d.ts new file mode 100644 index 00000000..99114e70 --- /dev/null +++ b/backend/tests/test-assets/learning-objects/learning-object-example.d.ts @@ -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} +}; diff --git a/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/Knop.png b/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/Knop.png new file mode 100644 index 00000000..34920e91 Binary files /dev/null and b/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/Knop.png differ diff --git a/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/content.md b/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/content.md new file mode 100644 index 00000000..0161bbd0 --- /dev/null +++ b/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/content.md @@ -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") diff --git a/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/dwengo.png b/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/dwengo.png new file mode 100644 index 00000000..b03fcd08 Binary files /dev/null and b/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/dwengo.png differ diff --git a/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/pn-werkingnotebooks-example.ts b/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/pn-werkingnotebooks-example.ts new file mode 100644 index 00000000..576f3d6c --- /dev/null +++ b/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/pn-werkingnotebooks-example.ts @@ -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; diff --git a/backend/tests/test-assets/learning-paths/learning-path-example.d.ts b/backend/tests/test-assets/learning-paths/learning-path-example.d.ts new file mode 100644 index 00000000..47d0221f --- /dev/null +++ b/backend/tests/test-assets/learning-paths/learning-path-example.d.ts @@ -0,0 +1,3 @@ +type LearningPathExample = { + createLearningPath: () => LearningPath +}; diff --git a/backend/tests/test-assets/learning-paths/learning-path-utils.ts b/backend/tests/test-assets/learning-paths/learning-path-utils.ts new file mode 100644 index 00000000..31374869 --- /dev/null +++ b/backend/tests/test-assets/learning-paths/learning-path-utils.ts @@ -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; +} diff --git a/backend/tests/test-assets/learning-paths/pn-werking-example.ts b/backend/tests/test-assets/learning-paths/pn-werking-example.ts new file mode 100644 index 00000000..dbf86052 --- /dev/null +++ b/backend/tests/test-assets/learning-paths/pn-werking-example.ts @@ -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; diff --git a/backend/tests/test-utils/expect-to-be-correct-entity.ts b/backend/tests/test-utils/expect-to-be-correct-entity.ts new file mode 100644 index 00000000..c978aab3 --- /dev/null +++ b/backend/tests/test-utils/expect-to-be-correct-entity.ts @@ -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( + 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}` + }); + } + } + } + } +} diff --git a/backend/tests/test-utils/load-test-asset.ts b/backend/tests/test-utils/load-test-asset.ts new file mode 100644 index 00000000..effa0c73 --- /dev/null +++ b/backend/tests/test-utils/load-test-asset.ts @@ -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}`)); +}