diff --git a/backend/src/services/learning-objects/processing/audio/audio-processor.ts b/backend/src/services/learning-objects/processing/audio/audio-processor.ts index d4e96c75..6442e1b3 100644 --- a/backend/src/services/learning-objects/processing/audio/audio-processor.ts +++ b/backend/src/services/learning-objects/processing/audio/audio-processor.ts @@ -1,12 +1,13 @@ /** * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/audio/audio_processor.js */ -import Processor from "../processor.js"; + import DOMPurify from 'isomorphic-dompurify'; import {type} from "node:os"; import {DwengoContentType} from "../content-type"; +import {StringProcessor} from "../string-processor"; -class AudioProcessor extends Processor { +class AudioProcessor extends StringProcessor { constructor() { super(DwengoContentType.AUDIO_MPEG); diff --git a/backend/src/services/learning-objects/processing/extern/extern-processor.ts b/backend/src/services/learning-objects/processing/extern/extern-processor.ts index 14bc0554..2aa02482 100644 --- a/backend/src/services/learning-objects/processing/extern/extern-processor.ts +++ b/backend/src/services/learning-objects/processing/extern/extern-processor.ts @@ -2,13 +2,13 @@ * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/extern/extern_processor.js */ -import Processor from "../processor.js"; import DOMPurify from 'isomorphic-dompurify'; import {ProcessingError} from "../processing-error"; import {isValidHttpUrl} from "../../../../util/links"; import {DwengoContentType} from "../content-type"; +import {StringProcessor} from "../string-processor"; -class ExternProcessor extends Processor { +class ExternProcessor extends StringProcessor { constructor() { super(DwengoContentType.EXTERN); } diff --git a/backend/src/services/learning-objects/processing/gift/gift-processor.ts b/backend/src/services/learning-objects/processing/gift/gift-processor.ts index 1f117d59..5cffcb47 100644 --- a/backend/src/services/learning-objects/processing/gift/gift-processor.ts +++ b/backend/src/services/learning-objects/processing/gift/gift-processor.ts @@ -2,7 +2,6 @@ * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/gift/gift_processor.js */ -import Processor from "../processor.js"; import DOMPurify from 'isomorphic-dompurify'; import {GIFTQuestion, parse} from "gift-pegjs" import {DwengoContentType} from "../content-type"; @@ -15,8 +14,9 @@ import {MatchingQuestionRenderer} from "./question-renderers/matching-question-r import {NumericalQuestionRenderer} from "./question-renderers/numerical-question-renderer"; import {ShortQuestionRenderer} from "./question-renderers/short-question-renderer"; import {TrueFalseQuestionRenderer} from "./question-renderers/true-false-question-renderer"; +import {StringProcessor} from "../string-processor"; -class GiftProcessor extends Processor { +class GiftProcessor extends StringProcessor { private renderers: RendererMap = { Category: new CategoryQuestionRenderer(), diff --git a/backend/src/services/learning-objects/processing/image/inline-image-processor.ts b/backend/src/services/learning-objects/processing/image/inline-image-processor.ts index 46a7f5ce..7815fab3 100644 --- a/backend/src/services/learning-objects/processing/image/inline-image-processor.ts +++ b/backend/src/services/learning-objects/processing/image/inline-image-processor.ts @@ -2,13 +2,13 @@ * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/image/inline_image_processor.js */ -import Processor from "../processor.js"; import DOMPurify from 'isomorphic-dompurify'; import {DwengoContentType} from "../content-type.js"; import {ProcessingError} from "../processing-error.js"; import {isValidHttpUrl} from "../../../../util/links"; +import {StringProcessor} from "../string-processor"; -class InlineImageProcessor extends Processor { +class InlineImageProcessor extends StringProcessor { constructor(contentType: DwengoContentType = DwengoContentType.IMAGE_INLINE) { super(contentType); } diff --git a/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts b/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts index 2dbe51d0..04b15b94 100644 --- a/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts +++ b/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts @@ -42,45 +42,43 @@ function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifier * - embeddings of other learning objects. */ const dwengoMarkedRenderer: RendererObject = { - heading(heading: Heading) { + heading(heading: Heading): string { const text = heading.text; const level = heading.depth; const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-'); - return ` - - - - - ${text} - `; + return `\n` + + ` \n` + + ` \n` + + ` \n` + + ` ${text}\n` + + `\n` }, // When the syntax for a link is used => [text](href "title") // render a custom link when the prefix for a learning object is used. - link(link: Link) { + link(link: Link): string { const href = link.href; const title = link.title || ""; - const text = link.text; + const text = marked.parseInline(link.text); // There could for example be an image in the link. if (href.startsWith(prefixes.learningObject)) { // link to learning-object const learningObjectId = extractLearningObjectIdFromHref(href); - return ` - ${text} - `; + return `${text}`; } else { // any other link if (!isValidHttpUrl(href)) { throw new ProcessingError("Link is not a valid HTTP URL!"); } + // return `${text}`; } }, // When the syntax for an image is used => ![text](href "title") // render a learning object, pdf, audio or video if a prefix is used. - image(img: Image) { + image(img: Image): string { const href = img.href; if (href.startsWith(prefixes.learningObject)) { // embedded 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 b1a49b6a..c20f08a3 100644 --- a/backend/src/services/learning-objects/processing/markdown/markdown-processor.ts +++ b/backend/src/services/learning-objects/processing/markdown/markdown-processor.ts @@ -2,14 +2,14 @@ * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/markdown/markdown_processor.js */ -import {marked} from 'marked' -import Processor from '../processor.js'; +import {marked} from 'marked'; import InlineImageProcessor from '../image/inline-image-processor.js'; import {DwengoContentType} from "../content-type"; -import {ProcessingError} from "../processing-error"; import dwengoMarkedRenderer from "./dwengo-marked-renderer"; +import {StringProcessor} from "../string-processor"; +import {ProcessingError} from "../processing-error"; -class MarkdownProcessor extends Processor { +class MarkdownProcessor extends StringProcessor { constructor() { super(DwengoContentType.TEXT_MARKDOWN); } @@ -17,9 +17,9 @@ class MarkdownProcessor extends Processor { override renderFn(mdText: string) { let html = ""; try { - mdText = this.replaceLinks(mdText); // Replace html image links with path based on metadata marked.use({renderer: dwengoMarkedRenderer}); html = marked(mdText, {async: false}); + html = this.replaceLinks(html); // Replace html image links path } catch (e: any) { throw new ProcessingError(e.message); } diff --git a/backend/src/services/learning-objects/processing/pdf/pdf-processor.ts b/backend/src/services/learning-objects/processing/pdf/pdf-processor.ts index a1defea8..fe05077e 100644 --- a/backend/src/services/learning-objects/processing/pdf/pdf-processor.ts +++ b/backend/src/services/learning-objects/processing/pdf/pdf-processor.ts @@ -2,13 +2,13 @@ * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/pdf/pdf_processor.js */ -import Processor from "../processor.js"; import DOMPurify from 'isomorphic-dompurify'; import {DwengoContentType} from "../content-type.js"; import {isValidHttpUrl} from "../../../../util/links.js"; import {ProcessingError} from "../processing-error.js"; +import {StringProcessor} from "../string-processor"; -class PdfProcessor extends Processor { +class PdfProcessor extends StringProcessor { constructor() { super(DwengoContentType.APPLICATION_PDF); } diff --git a/backend/src/services/learning-objects/processing/processor.ts b/backend/src/services/learning-objects/processing/processor.ts index caffcb3e..3e511cd9 100644 --- a/backend/src/services/learning-objects/processing/processor.ts +++ b/backend/src/services/learning-objects/processing/processor.ts @@ -48,7 +48,6 @@ abstract class Processor { /** * Function which actually executes the rendering of a learning object. - * By default, this just means rendering the content in the content property of the learning object. * * When implementing this function, we may assume that we are responsible for the content type of the learning * object. @@ -56,9 +55,7 @@ abstract class Processor { * @param toRender Learning object to render * @protected */ - protected renderLearningObjectFn(toRender: LearningObject): string { - return this.render(toRender.content as T); - } + protected abstract renderLearningObjectFn(toRender: LearningObject): string; } export default Processor; diff --git a/backend/src/services/learning-objects/processing/string-processor.ts b/backend/src/services/learning-objects/processing/string-processor.ts new file mode 100644 index 00000000..2026fa93 --- /dev/null +++ b/backend/src/services/learning-objects/processing/string-processor.ts @@ -0,0 +1,19 @@ +import Processor from "./processor"; +import {LearningObject} from "../../../entities/content/learning-object.entity"; + +export abstract class StringProcessor extends Processor { + /** + * Function which actually executes the rendering of a learning object. + * By default, this just means rendering the content in the content property of the learning object (interpreted + * as string) + * + * When implementing this function, we may assume that we are responsible for the content type of the learning + * object. + * + * @param toRender Learning object to render + * @protected + */ + protected renderLearningObjectFn(toRender: LearningObject): string { + return this.render(toRender.content.toString("ascii")); + } +} diff --git a/backend/src/services/learning-objects/processing/text/text-processor.ts b/backend/src/services/learning-objects/processing/text/text-processor.ts index ed67e93a..fb3922a7 100644 --- a/backend/src/services/learning-objects/processing/text/text-processor.ts +++ b/backend/src/services/learning-objects/processing/text/text-processor.ts @@ -2,11 +2,11 @@ * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/text/text_processor.js */ -import DOMPurify from 'isomorphic-dompurify' -import Processor from "../processor.js" +import DOMPurify from 'isomorphic-dompurify'; import {DwengoContentType} from "../content-type.js"; +import {StringProcessor} from "../string-processor"; -class TextProcessor extends Processor { +class TextProcessor extends StringProcessor { constructor() { super(DwengoContentType.TEXT_PLAIN); } diff --git a/backend/src/util/links.ts b/backend/src/util/links.ts index c58ac5f1..5deb21c2 100644 --- a/backend/src/util/links.ts +++ b/backend/src/util/links.ts @@ -2,7 +2,7 @@ import {LearningObjectIdentifier} from "../interfaces/learning-content"; export function isValidHttpUrl(url: string): boolean { try { - const parsedUrl = new URL(url); + const parsedUrl = new URL(url, "http://test.be"); return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"; } catch (e) { return false; diff --git a/backend/tests/services/learning-objects/database-learning-object-provider.test.ts b/backend/tests/services/learning-objects/database-learning-object-provider.test.ts index 509b5e2e..c8783246 100644 --- a/backend/tests/services/learning-objects/database-learning-object-provider.test.ts +++ b/backend/tests/services/learning-objects/database-learning-object-provider.test.ts @@ -25,28 +25,34 @@ describe("DatabaseLearningObjectProvider", () => { await setupTestApp(); exampleLearningObject = await initExampleData(); }); - - it("should return the learning object when it is queried by its id", async () => { - const result: FilteredLearningObject | null = await databaseLearningObjectProvider.getLearningObjectById(exampleLearningObject); - expect(result).toBeTruthy(); - expectToBeCorrectFilteredLearningObject(result!, exampleLearningObject); - }); - - it("should return the learning object when it is queried by only hruid and language (but not version)", async () => { - const result: FilteredLearningObject | null = await databaseLearningObjectProvider.getLearningObjectById({ - hruid: exampleLearningObject.hruid, - language: exampleLearningObject.language + describe("getLearningObjectById", () => { + it("should return the learning object when it is queried by its id", async () => { + const result: FilteredLearningObject | null = await databaseLearningObjectProvider.getLearningObjectById(exampleLearningObject); + expect(result).toBeTruthy(); + expectToBeCorrectFilteredLearningObject(result!, exampleLearningObject); }); - expect(result).toBeTruthy(); - expectToBeCorrectFilteredLearningObject(result!, exampleLearningObject); - }); - it("should return null when queried with an id that does not exist", async () => { - const result: FilteredLearningObject | null = await databaseLearningObjectProvider.getLearningObjectById({ - hruid: "non_existing_hruid", - language: Language.Dutch + it("should return the learning object when it is queried by only hruid and language (but not version)", async () => { + const result: FilteredLearningObject | null = await databaseLearningObjectProvider.getLearningObjectById({ + hruid: exampleLearningObject.hruid, + language: exampleLearningObject.language + }); + expect(result).toBeTruthy(); + expectToBeCorrectFilteredLearningObject(result!, exampleLearningObject); }); - expect(result).toBeNull(); - }); + it("should return null when queried with an id that does not exist", async () => { + const result: FilteredLearningObject | null = await databaseLearningObjectProvider.getLearningObjectById({ + hruid: "non_existing_hruid", + language: Language.Dutch + }); + expect(result).toBeNull(); + }); + }); + describe("getLearningObjectHTML", () => { + it("should return the correct rendering of the learning object", async () => { + const result = await databaseLearningObjectProvider.getLearningObjectHTML(exampleLearningObject); + expect(result).toEqual(example.getHTMLRendering()); + }); + }); }); 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 index 99114e70..90808fe9 100644 --- a/backend/tests/test-assets/learning-objects/learning-object-example.d.ts +++ b/backend/tests/test-assets/learning-objects/learning-object-example.d.ts @@ -3,5 +3,6 @@ import {Attachment} from "../../../src/entities/content/attachment.entity"; type LearningObjectExample = { createLearningObject: () => LearningObject, - createAttachment: {[key: string]: (owner: LearningObject) => Attachment} + createAttachment: {[key: string]: (owner: LearningObject) => Attachment}, + getHTMLRendering: () => string }; 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 index abca3ab3..4dff2c34 100644 --- 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 @@ -45,7 +45,7 @@ const example: LearningObjectExample = { learningObject.returnValue = returnValue; learningObject.available = true; - learningObject.content = loadTestAsset(`${ASSETS_PREFIX}/dwengo.png`); + learningObject.content = loadTestAsset(`${ASSETS_PREFIX}/content.md`); return learningObject }, @@ -66,6 +66,7 @@ const example: LearningObjectExample = { att.content = loadTestAsset(`${ASSETS_PREFIX}/Knop.png`) return att; } - } + }, + getHTMLRendering: () => loadTestAsset(`${ASSETS_PREFIX}/rendering.html`).toString() } export default example; diff --git a/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/rendering.html b/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/rendering.html new file mode 100644 index 00000000..d197ebd4 --- /dev/null +++ b/backend/tests/test-assets/learning-objects/pn_werkingnotebooks/rendering.html @@ -0,0 +1,20 @@ +

+ + + + 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 intuC/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 C)C)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).

+
+

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.

+