From 1417907933cc8921e6a02dd71d72dfbdeb28586a Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Sun, 9 Mar 2025 08:50:39 +0100 Subject: [PATCH] fix(backend): Foute entity-structuur van leerpaden verbeterd. Ook testen geschreven voor LearningPathRepository en LearningObjectRepository. --- .../content/learning-object-repository.ts | 1 - .../data/content/learning-path-repository.ts | 8 +- .../content/learning-object.entity.ts | 2 +- .../content/learning-path-node.entity.ts | 37 +++++++++ .../learning-path-transition.entity.ts | 17 ++++ .../entities/content/learning-path.entity.ts | 48 ++--------- .../learning-objects/attachment-service.ts | 4 +- .../database-learning-object-provider.ts | 10 ++- ...-renderer.ts => dwengo-marked-renderer.ts} | 13 ++- .../processing/markdown/markdown-processor.ts | 3 +- .../processing/processing-service.ts | 6 +- .../database-learning-path-provider.ts | 6 +- .../learning-object-repository.test.ts | 71 ++++++++++++++++ .../content/learning-path-repository.test.ts | 78 ++++++++++++++++++ .../learning-object-example.d.ts | 7 ++ .../pn_werkingnotebooks/Knop.png | Bin 0 -> 7784 bytes .../pn_werkingnotebooks/content.md | 25 ++++++ .../pn_werkingnotebooks/dwengo.png | Bin 0 -> 31571 bytes .../pn-werkingnotebooks-example.ts | 74 +++++++++++++++++ .../learning-paths/learning-path-example.d.ts | 3 + .../learning-paths/learning-path-utils.ts | 24 ++++++ .../learning-paths/pn-werking-example.ts | 29 +++++++ .../test-utils/expect-to-be-correct-entity.ts | 62 ++++++++++++++ backend/tests/test-utils/load-test-asset.ts | 10 +++ 24 files changed, 474 insertions(+), 64 deletions(-) create mode 100644 backend/src/entities/content/learning-path-node.entity.ts create mode 100644 backend/src/entities/content/learning-path-transition.entity.ts rename backend/src/services/learning-objects/processing/markdown/{learning-object-markdown-renderer.ts => dwengo-marked-renderer.ts} (92%) create mode 100644 backend/tests/data/content/learning-object-repository.test.ts create mode 100644 backend/tests/data/content/learning-path-repository.test.ts create mode 100644 backend/tests/test-assets/learning-objects/learning-object-example.d.ts create mode 100644 backend/tests/test-assets/learning-objects/pn_werkingnotebooks/Knop.png create mode 100644 backend/tests/test-assets/learning-objects/pn_werkingnotebooks/content.md create mode 100644 backend/tests/test-assets/learning-objects/pn_werkingnotebooks/dwengo.png create mode 100644 backend/tests/test-assets/learning-objects/pn_werkingnotebooks/pn-werkingnotebooks-example.ts create mode 100644 backend/tests/test-assets/learning-paths/learning-path-example.d.ts create mode 100644 backend/tests/test-assets/learning-paths/learning-path-utils.ts create mode 100644 backend/tests/test-assets/learning-paths/pn-werking-example.ts create mode 100644 backend/tests/test-utils/expect-to-be-correct-entity.ts create mode 100644 backend/tests/test-utils/load-test-asset.ts 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 0000000000000000000000000000000000000000..34920e91646e0e5f0f29675a47ec6ad62b1cd3d2 GIT binary patch literal 7784 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b03fcd081c8af485f0e81de5394c05b1d559a7ca GIT binary patch literal 31571 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 { + 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}`)); +}