test(backend): Testen voor DatabaseLearningObjectProvider.getLearningObjectHTML toegevoegd.

Hierbij optredende problemen ook opgelost.
This commit is contained in:
Gerald Schmittinger 2025-03-09 19:29:20 +01:00
parent a3b995393b
commit 91e3b5ad91
15 changed files with 103 additions and 60 deletions

View file

@ -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<string> {
class AudioProcessor extends StringProcessor {
constructor() {
super(DwengoContentType.AUDIO_MPEG);

View file

@ -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<string> {
class ExternProcessor extends StringProcessor {
constructor() {
super(DwengoContentType.EXTERN);
}

View file

@ -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<string> {
class GiftProcessor extends StringProcessor {
private renderers: RendererMap = {
Category: new CategoryQuestionRenderer(),

View file

@ -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<string> {
class InlineImageProcessor extends StringProcessor {
constructor(contentType: DwengoContentType = DwengoContentType.IMAGE_INLINE) {
super(contentType);
}

View file

@ -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 `
<h${level}>
<a name="${escapedText}" class="anchor" href="#${escapedText}">
<span class="header-link"></span>
</a>
${text}
</h${level}>`;
return `<h${level}>\n` +
` <a name="${escapedText}" class="anchor" href="#${escapedText}">\n` +
` <span class="header-link"></span>\n` +
` </a>\n` +
` ${text}\n` +
`</h${level}>\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 `
<a href="${getUrlStringForLearningObjectHTML(learningObjectId)}]" target="_blank" title="${title}">${text}</a>
`;
return `<a href="${getUrlStringForLearningObjectHTML(learningObjectId)}" target="_blank" title="${title}">${text}</a>`;
} else {
// any other link
if (!isValidHttpUrl(href)) {
throw new ProcessingError("Link is not a valid HTTP URL!");
}
//<a href="https://kiks.ilabt.imec.be/hub/tmplogin?id=0101" title="Notebooks Werking"><img src="Knop.png" alt="" title="Knop"></a>
return `<a href="${href}" target="_blank" title="${title}">${text}</a>`;
}
},
// 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

View file

@ -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<string> {
class MarkdownProcessor extends StringProcessor {
constructor() {
super(DwengoContentType.TEXT_MARKDOWN);
}
@ -17,9 +17,9 @@ class MarkdownProcessor extends Processor<string> {
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);
}

View file

@ -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<string> {
class PdfProcessor extends StringProcessor {
constructor() {
super(DwengoContentType.APPLICATION_PDF);
}

View file

@ -48,7 +48,6 @@ abstract class Processor<T> {
/**
* 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<T> {
* @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;

View file

@ -0,0 +1,19 @@
import Processor from "./processor";
import {LearningObject} from "../../../entities/content/learning-object.entity";
export abstract class StringProcessor extends Processor<string> {
/**
* 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"));
}
}

View file

@ -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<string> {
class TextProcessor extends StringProcessor {
constructor() {
super(DwengoContentType.TEXT_PLAIN);
}

View file

@ -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;

View file

@ -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());
});
});
});

View file

@ -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
};

View file

@ -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;

View file

@ -0,0 +1,20 @@
<h1>
<a name="werken-met-notebooks" class="anchor" href="#werken-met-notebooks">
<span class="header-link"></span>
</a>
Werken met notebooks
</h1>
<p>Het lesmateriaal van &#39;Python in wiskunde en STEM&#39; wordt aangeboden in de vorm van interactieve <strong><em>notebooks</em></strong>. Notebooks zijn <em>digitale documenten</em> die zowel uitvoerbare code bevatten als tekst, afbeeldingen, video, hyperlinks ...</p>
<p><em>Nieuwe begrippen</em> worden aangebracht via tekstuele uitleg, video en afbeeldingen.</p>
<p>Er zijn uitgewerkte <em>voorbeelden</em> met daarnaast ook kleine en grote <em>opdrachten</em>. In deze opdrachten zal je aangereikte code kunnen uitvoeren, maar ook zelf code opstellen.</p>
<p>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.</p>
<p>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 (<a href="https://libstore.ugent.be/fulltxt/RUG01/003/151/437/RUG01-003151437_2023_0001_AC.pdf" target="_blank" title="">Jeroen Van der Hooft, 2023</a>).</p>
<hr>
<p>Klik je op onderstaande knop &#39;Open notebooks&#39;, dan word je doorgestuurd naar een andere website waar jouw persoonlijke notebooks ingeladen worden. (Dit kan even duren.)</p>
<p>Links op het scherm vind je er twee bestanden met extensie <em>.ipynb</em>.
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.</p>
<p>Je ziet er ook een map <em>images</em> met de afbeeldingen die in de notebooks getoond worden.</p>
<p>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.</p>
<p><a href="https://kiks.ilabt.imec.be/hub/tmplogin?id=0101" target="_blank" title="Notebooks Werking"><img alt="" src="Knop.png"></a></p>