diff --git a/backend/package.json b/backend/package.json index bacdac6c..872f5495 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,13 +17,16 @@ "@mikro-orm/core": "^6.4.6", "@mikro-orm/postgresql": "^6.4.6", "@mikro-orm/reflection": "^6.4.6", + "@mikro-orm/sqlite": "6.4.6", "@types/js-yaml": "^4.0.9", "axios": "^1.8.1", - "@mikro-orm/sqlite": "6.4.6", "dotenv": "^16.4.7", "express": "^5.0.1", - "uuid": "^11.1.0", + "gift-pegjs": "^1.0.2", + "isomorphic-dompurify": "^2.22.0", "js-yaml": "^4.1.0", + "marked": "^15.0.7", + "uuid": "^11.1.0" }, "devDependencies": { "@mikro-orm/cli": "^6.4.6", diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index c4dd8d08..0445d9f1 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -5,6 +5,8 @@ import learningObjectService from "../services/learning-objects/learning-object- import {EnvVars, getEnvVar} from "../util/envvars"; import {Language} from "../entities/content/language"; import {BadRequestException} from "../exceptions"; +import attachmentService from "../services/learning-objects/attachment-service"; +import {NotFoundError} from "@mikro-orm/core"; function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { if (!req.params.hruid) { @@ -60,3 +62,14 @@ export async function getLearningObjectHTML(req: Request, res: Response): Promis const learningObject = await learningObjectService.getLearningObjectHTML(learningObjectId); res.send(learningObject); } + +export async function getAttachment(req: Request, res: Response): Promise { + const learningObjectId = getLearningObjectIdentifierFromRequest(req); + const name = req.params.attachmentName; + const attachment = await attachmentService.getAttachment(learningObjectId, name); + + if (!attachment) { + throw new NotFoundError(`Attachment ${name} not found`); + } + res.setHeader("Content-Type", attachment.mimeType).send(attachment.content) +} diff --git a/backend/src/data/content/attachment-repository.ts b/backend/src/data/content/attachment-repository.ts index 3268be90..c7a53c86 100644 --- a/backend/src/data/content/attachment-repository.ts +++ b/backend/src/data/content/attachment-repository.ts @@ -1,15 +1,40 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Attachment } from '../../entities/content/attachment.entity.js'; -import { LearningObject } from '../../entities/content/learning-object.entity.js'; +import {Language} from "../../entities/content/language"; +import {LearningObjectIdentifier} from "../../entities/content/learning-object-identifier"; export class AttachmentRepository extends DwengoEntityRepository { - public findByLearningObjectAndNumber( - learningObject: LearningObject, - sequenceNumber: number - ) { + public findByLearningObjectIdAndName( + learningObjectId: LearningObjectIdentifier, + name: string + ): Promise { return this.findOne({ - learningObject: learningObject, - sequenceNumber: sequenceNumber, + learningObject: { + hruid: learningObjectId.hruid, + language: learningObjectId.language, + version: learningObjectId.version, + }, + name: name, + }); + } + + public findByMostRecentVersionOfLearningObjectAndName( + hruid: string, + language: Language, + attachmentName: string + ): Promise { + return this.findOne({ + learningObject: { + hruid: hruid, + language: language + }, + name: attachmentName + }, { + orderBy: { + learningObject: { + version: 'DESC' + } + } }); } // This repository is read-only for now since creating own learning object is an extension feature. diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index 5d30b956..331ff8e2 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -1,6 +1,7 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; +import {Language} from "../../entities/content/language"; export class LearningObjectRepository extends DwengoEntityRepository { public findByIdentifier( @@ -12,5 +13,16 @@ export class LearningObjectRepository extends DwengoEntityRepository AttachmentRepository}) export class Attachment { @ManyToOne({ entity: () => LearningObject, primary: true }) learningObject!: LearningObject; - @PrimaryKey({ type: 'integer' }) - sequenceNumber!: number; + @PrimaryKey({ type: 'string' }) + name!: string; @Property({ type: 'string' }) mimeType!: string; diff --git a/backend/src/entities/content/language.ts b/backend/src/entities/content/language.ts index b5d18c80..7a106762 100644 --- a/backend/src/entities/content/language.ts +++ b/backend/src/entities/content/language.ts @@ -1,6 +1,186 @@ export enum Language { + Afar = 'aa', + Abkhazian = 'ab', + Afrikaans = 'af', + Akan = 'ak', + Albanian = 'sq', + Amharic = 'am', + Arabic = 'ar', + Aragonese = 'an', + Armenian = 'hy', + Assamese = 'as', + Avaric = 'av', + Avestan = 'ae', + Aymara = 'ay', + Azerbaijani = 'az', + Bashkir = 'ba', + Bambara = 'bm', + Basque = 'eu', + Belarusian = 'be', + Bengali = 'bn', + Bihari = 'bh', + Bislama = 'bi', + Bosnian = 'bs', + Breton = 'br', + Bulgarian = 'bg', + Burmese = 'my', + Catalan = 'ca', + Chamorro = 'ch', + Chechen = 'ce', + Chinese = 'zh', + ChurchSlavic = 'cu', + Chuvash = 'cv', + Cornish = 'kw', + Corsican = 'co', + Cree = 'cr', + Czech = 'cs', + Danish = 'da', + Divehi = 'dv', Dutch = 'nl', - French = 'fr', + Dzongkha = 'dz', English = 'en', - Germany = 'de', + Esperanto = 'eo', + Estonian = 'et', + Ewe = 'ee', + Faroese = 'fo', + Fijian = 'fj', + Finnish = 'fi', + French = 'fr', + Frisian = 'fy', + Fulah = 'ff', + Georgian = 'ka', + German = 'de', + Gaelic = 'gd', + Irish = 'ga', + Galician = 'gl', + Manx = 'gv', + Greek = 'el', + Guarani = 'gn', + Gujarati = 'gu', + Haitian = 'ht', + Hausa = 'ha', + Hebrew = 'he', + Herero = 'hz', + Hindi = 'hi', + HiriMotu = 'ho', + Croatian = 'hr', + Hungarian = 'hu', + Igbo = 'ig', + Icelandic = 'is', + Ido = 'io', + SichuanYi = 'ii', + Inuktitut = 'iu', + Interlingue = 'ie', + Interlingua = 'ia', + Indonesian = 'id', + Inupiaq = 'ik', + Italian = 'it', + Javanese = 'jv', + Japanese = 'ja', + Kalaallisut = 'kl', + Kannada = 'kn', + Kashmiri = 'ks', + Kanuri = 'kr', + Kazakh = 'kk', + Khmer = 'km', + Kikuyu = 'ki', + Kinyarwanda = 'rw', + Kirghiz = 'ky', + Komi = 'kv', + Kongo = 'kg', + Korean = 'ko', + Kuanyama = 'kj', + Kurdish = 'ku', + Lao = 'lo', + Latin = 'la', + Latvian = 'lv', + Limburgan = 'li', + Lingala = 'ln', + Lithuanian = 'lt', + Luxembourgish = 'lb', + LubaKatanga = 'lu', + Ganda = 'lg', + Macedonian = 'mk', + Marshallese = 'mh', + Malayalam = 'ml', + Maori = 'mi', + Marathi = 'mr', + Malay = 'ms', + Malagasy = 'mg', + Maltese = 'mt', + Mongolian = 'mn', + Nauru = 'na', + Navajo = 'nv', + SouthNdebele = 'nr', + NorthNdebele = 'nd', + Ndonga = 'ng', + Nepali = 'ne', + NorwegianNynorsk = 'nn', + NorwegianBokmal = 'nb', + Norwegian = 'no', + Chichewa = 'ny', + Occitan = 'oc', + Ojibwa = 'oj', + Oriya = 'or', + Oromo = 'om', + Ossetian = 'os', + Punjabi = 'pa', + Persian = 'fa', + Pali = 'pi', + Polish = 'pl', + Portuguese = 'pt', + Pashto = 'ps', + Quechua = 'qu', + Romansh = 'rm', + Romanian = 'ro', + Rundi = 'rn', + Russian = 'ru', + Sango = 'sg', + Sanskrit = 'sa', + Sinhala = 'si', + Slovak = 'sk', + Slovenian = 'sl', + NorthernSami = 'se', + Samoan = 'sm', + Shona = 'sn', + Sindhi = 'sd', + Somali = 'so', + Sotho = 'st', + Spanish = 'es', + Sardinian = 'sc', + Serbian = 'sr', + Swati = 'ss', + Sundanese = 'su', + Swahili = 'sw', + Swedish = 'sv', + Tahitian = 'ty', + Tamil = 'ta', + Tatar = 'tt', + Telugu = 'te', + Tajik = 'tg', + Tagalog = 'tl', + Thai = 'th', + Tibetan = 'bo', + Tigrinya = 'ti', + Tonga = 'to', + Tswana = 'tn', + Tsonga = 'ts', + Turkmen = 'tk', + Turkish = 'tr', + Twi = 'tw', + Uighur = 'ug', + Ukrainian = 'uk', + Urdu = 'ur', + Uzbek = 'uz', + Venda = 've', + Vietnamese = 'vi', + Volapuk = 'vo', + Welsh = 'cy', + Walloon = 'wa', + Wolof = 'wo', + Xhosa = 'xh', + Yiddish = 'yi', + Yoruba = 'yo', + Zhuang = 'za', + Zulu = 'zu' } diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index 4cf3f163..1f888a19 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -11,7 +11,7 @@ import { import { Language } from './language.js'; import { Attachment } from './attachment.entity.js'; import { Teacher } from '../users/teacher.entity.js'; -import {DwengoContentType} from "../../services/learning-objects/processing/content_type"; +import {DwengoContentType} from "../../services/learning-objects/processing/content-type"; @Entity() export class LearningObject { diff --git a/backend/src/routes/learning-objects.ts b/backend/src/routes/learning-objects.ts index b44a6b8f..421a5f19 100644 --- a/backend/src/routes/learning-objects.ts +++ b/backend/src/routes/learning-objects.ts @@ -1,6 +1,6 @@ import express from 'express'; import { - getAllLearningObjects, + getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML, } from '../controllers/learning-objects.js'; @@ -30,4 +30,10 @@ router.get('/:hruid', getLearningObject); // Example: http://localhost:3000/learningObject/un_ai7/html router.get('/:hruid/html', getLearningObjectHTML); +// Parameter: hruid of learning object, name of attachment. +// Query: language, version (optional). +// Route to get the raw data of the attachment for one learning object based on its hruid. +// Example: http://localhost:3000/learningObject/u_test/attachment/testimage.png +router.get('/:hruid/html/:attachmentName', getAttachment); + export default router; diff --git a/backend/src/services/learning-objects/attachment-service.ts b/backend/src/services/learning-objects/attachment-service.ts new file mode 100644 index 00000000..f783663a --- /dev/null +++ b/backend/src/services/learning-objects/attachment-service.ts @@ -0,0 +1,21 @@ +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 { + if (learningObjectId.version) { + return attachmentRepo.findByLearningObjectIdAndName({ + hruid: learningObjectId.hruid, + language: learningObjectId.language, + version: learningObjectId.version, + }, attachmentName); + } else { + return attachmentRepo.findByMostRecentVersionOfLearningObjectAndName(learningObjectId.hruid, learningObjectId.language, attachmentName); + } + } +} + +export default attachmentService; diff --git a/backend/src/services/learning-objects/processing/audio/audio-processor.ts b/backend/src/services/learning-objects/processing/audio/audio-processor.ts new file mode 100644 index 00000000..d4e96c75 --- /dev/null +++ b/backend/src/services/learning-objects/processing/audio/audio-processor.ts @@ -0,0 +1,23 @@ +/** + * 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"; + +class AudioProcessor extends Processor { + + constructor() { + super(DwengoContentType.AUDIO_MPEG); + } + + protected renderFn(audioUrl: string): string { + return DOMPurify.sanitize(``); + } +} + +export default AudioProcessor; diff --git a/backend/src/services/learning-objects/processing/audio/audio_processor.js b/backend/src/services/learning-objects/processing/audio/audio_processor.js deleted file mode 100644 index d41ed1ce..00000000 --- a/backend/src/services/learning-objects/processing/audio/audio_processor.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Based on - */ -import Processor from "../processor.ts"; -import { isValidHttpUrl } from '../../utils/utils.js' -import { findFile } from '../../utils/file_io.js' -import InvalidArgumentError from '../../utils/invalid_argument_error.js' -import DOMPurify from 'isomorphic-dompurify'; -import ProcessingHistory from "../../models/processing_history.js"; -import path from "path" -import fs from "fs" - -class AudioProcessor extends Processor { - - constructor() { - super(); - this.types = ["audio/mpeg"] // TODO add functionality to accept other audio types (ogg, wav) - } - - /** - * - * @param {string} audioUrl - * @param {object} args Optional arguments specific to the render function of the AudioProcessor - * @returns - */ - render(audioUrl, args = { files: [], metadata: {} }) { - - if ((!args.files || args.files.length <= 0 || !findFile(audioUrl, args.files)) && !isValidHttpUrl(audioUrl)) { - if (args.metadata && args.metadata.hruid && args.metadata.version && args.metadata.language){ - ProcessingHistory.error(args.metadata.hruid, args.metadata.version, args.metadata.language, "The audio file cannot be found. Please check if the url is spelled correctly.") - }else{ - ProcessingHistory.error("generalError", "99999999", "en", "The audio file cannot be found. Please check if the url is spelled correctly.") - } - - throw new InvalidArgumentError("The audio file cannot be found. Please check if the url is spelled correctly."); - } - - let type; - if (!args.metadata || !args.metadata.content_type || !this.types.includes(args.metadata.content_type)) { - type = this.types[0]; - } else { - type = args.metadata.content_type; - } - - if (isValidHttpUrl(audioUrl)) { - return DOMPurify.sanitize(``); - } - - if (!args.metadata._id) { - throw new InvalidArgumentError("The metadata for for the object which uses the file '" + audioUrl + "' is not loaded in the processor."); - } - - return DOMPurify.sanitize(``); - - } - - processFiles(files, metadata){ - let args = {} - let inputString = ""; - let file = files.find((f) => { - let ext = path.extname(f.originalname); - if (ext == ".mp3") { - inputString = f["originalname"] - // add files to args to check if file exists - args.files = files; - args.metadata = metadata - return true; - }else{ - return false; - } - }); - return [this.render(inputString, args), files] - } -} - -export default AudioProcessor; diff --git a/backend/src/services/learning-objects/processing/blockly/blockly_processor.js b/backend/src/services/learning-objects/processing/blockly/blockly_processor.js deleted file mode 100644 index 45296454..00000000 --- a/backend/src/services/learning-objects/processing/blockly/blockly_processor.js +++ /dev/null @@ -1,83 +0,0 @@ -import Processor from "../processor.ts"; -import { isValidHttpUrl } from '../../utils/utils.js' -import InvalidArgumentError from '../../utils/invalid_argument_error.js' -import DOMPurify from 'isomorphic-dompurify'; -import Logger from "../../logger.js"; -import path from "path" - -let logger = Logger.getLogger() -class BlocklyProcessor extends Processor { - constructor() { - super(); - this.blockly_base_url = process.env.SIMULATOR_READONLY_BASE_PATH; - } - - /** - * - * @param {string} blocklyXml - * @param {object} args Optional arguments specific to the render function of the BlocklyProcessor - * @returns - */ - render(blocklyXml, args = { language: "nl", id: "" }, {height = 315, aspect_ratio = 'iframe-16-9'} = {}) { - if (!args.language || args.language.trim() == "") { - args.language = "nl"; - } - if (!args.id || args.id.trim() == "") { - throw new InvalidArgumentError("The unique object id must be passed to the blockly processor."); - } - let languages = ["aa", "ab", "af", "ak", "sq", "am", "ar", "an", "hy", "as", "av", "ae", "ay", "az", "ba", "bm", "eu", "be", "bn", "bh", "bi", "bs", "br", "bg", "my", "ca", "ch", "ce", "zh", "cu", "cv", "kw", "co", "cr", "cs", "da", "dv", "nl", "dz", "en", "eo", "et", "ee", "fo", "fj", "fi", "fr", "fy", "ff", "ka", "de", "gd", "ga", "gl", "gv", "el", "gn", "gu", "ht", "ha", "he", "hz", "hi", "ho", "hr", "hu", "ig", "is", "io", "ii", "iu", "ie", "ia", "id", "ik", "it", "jv", "ja", "kl", "kn", "ks", "kr", "kk", "km", "ki", "rw", "ky", "kv", "kg", "ko", "kj", "ku", "lo", "la", "lv", "li", "ln", "lt", "lb", "lu", "lg", "mk", "mh", "ml", "mi", "mr", "ms", "mg", "mt", "mn", "na", "nv", "nr", "nd", "ng", "ne", "nn", "nb", "no", "ny", "oc", "oj", "or", "om", "os", "pa", "fa", "pi", "pl", "pt", "ps", "qu", "rm", "ro", "rn", "ru", "sg", "sa", "si", "sk", "sl", "se", "sm", "sn", "sd", "so", "st", "es", "sc", "sr", "ss", "su", "sw", "sv", "ty", "ta", "tt", "te", "tg", "tl", "th", "bo", "ti", "to", "tn", "ts", "tk", "tr", "tw", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "cy", "wa", "wo", "xh", "yi", "yo", "za", "zu"]; - if (!languages.includes(args.language)) { - throw new InvalidArgumentError("The language must be valid. " + args.language + " is not a supported language.") - } - if (typeof blocklyXml == 'undefined') { - throw new InvalidArgumentError("The blockly XML is undefined. Please provide correct XML code.") - } - - let simulatorUrl = `${this.blockly_base_url}` - - let form = ` -
- -
- ` - - let iframe = ` -
- ` - - let code = `(function(){ - var auto = setTimeout(function(){ submitform(); }, 50); - - function submitform(){ - document.forms["blockly_form_${args.id}"].submit(); - } - })() - ` - - let script = `` - - let html = form + iframe // DOMPurify.sanitize(form + iframe, {ALLOW_UNKNOWN_PROTOCOLS: true, ADD_TAGS: ["iframe", "xml"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'target']}); - html = html + script; - - return html; //TODO is not sanitized using DOMPurify.sanitize (problems with script tags) - } - - processFiles(files, metadata){ - let args = {} - let inputString = ""; - let file = files.find((f) => { - let ext = path.extname(f.originalname); - if (ext == ".xml") { - inputString = f.buffer.toString('utf8'); - args.language = metadata.language; - args.id = metadata._id; - return true; - }else{ - return false; - } - }); - return [this.render(inputString, args), files] - } -} - -export default BlocklyProcessor; diff --git a/backend/src/services/learning-objects/processing/content_type.ts b/backend/src/services/learning-objects/processing/content-type.ts similarity index 100% rename from backend/src/services/learning-objects/processing/content_type.ts rename to backend/src/services/learning-objects/processing/content-type.ts diff --git a/backend/src/services/learning-objects/processing/ct_schema/ct_schema_processor.js b/backend/src/services/learning-objects/processing/ct_schema/ct_schema_processor.js deleted file mode 100644 index 78c3dbc7..00000000 --- a/backend/src/services/learning-objects/processing/ct_schema/ct_schema_processor.js +++ /dev/null @@ -1,67 +0,0 @@ -import Logger from '../../logger.js'; -import InvalidArgumentError from "../../utils/invalid_argument_error.js"; -import { MarkdownProcessor } from '../markdown/markdown_processor.js'; - -class CTSchemaProcessor extends MarkdownProcessor{ - logger = Logger.getLogger(); - constructor(args = { files: [], metadata: {} }) { - super(); - this.staticPath = `${process.env.DOMAIN_URL}${process.env.STATIC_BASE_PATH}/img/ct_schema/`; - } - - /** - * - * @param {string} mdText a string containing the content for the four ct schema in markdown - * @returns The sanitized version of the generated html. - */ - render(text, args = {}) { - let html = ""; - // 1. Split text into markdown parts for each CT aspect - // 2. Call super.render on the individual parts - // 3. Group the parts together with specific html structure - - const regexObject = { - context: /([\s\S]*?)<\/context>/, - decomp: /([\s\S]*?)<\/decomposition>/, - abstr: /([\s\S]*?)<\/abstraction>/, - pattern: /([\s\S]*?)<\/patternRecognition>/, - algo: /([\s\S]*?)<\/algorithms>/, - impl: /([\s\S]*?)<\/implementation>/, - } - - let htmlObject = {} - - let htmlStructure = (valueObject) => ` -
-
${valueObject.context}
-
-
${valueObject.decomp}
-
${valueObject.pattern}
-
-
-
${valueObject.abstr}
-
${valueObject.algo}
-
-
${valueObject.impl}
-
` - - try { - for (let key in regexObject) { - let match = text.match(regexObject[key]); - if (match && match.length >= 1){ - htmlObject[key] = super.render(match[1]); - }else{ - htmlObject[key] = ""; - } - } - } catch (e) { - throw new InvalidArgumentError(e.message); - return "" - } - return htmlStructure(htmlObject); - } - -} - - -export { CTSchemaProcessor }; \ No newline at end of file diff --git a/backend/src/services/learning-objects/processing/extern/extern-processor.ts b/backend/src/services/learning-objects/processing/extern/extern-processor.ts new file mode 100644 index 00000000..14bc0554 --- /dev/null +++ b/backend/src/services/learning-objects/processing/extern/extern-processor.ts @@ -0,0 +1,38 @@ +/** + * 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"; + +class ExternProcessor extends Processor { + constructor() { + super(DwengoContentType.EXTERN); + } + + override renderFn(externURL: string) { + if (!isValidHttpUrl(externURL)) { + throw new ProcessingError("The url is not valid: " + externURL); + } + + // If a seperate youtube-processor would be added, this code would need to move to that processor + // Converts youtube urls to youtube-embed urls + let match = /(.*youtube.com\/)watch\?v=(.*)/.exec(externURL) + if (match) { + externURL = match[1] + "embed/" + match[2]; + } + + return DOMPurify.sanitize(` +
+ +
`, + { ADD_TAGS: ["iframe"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling']} + ); + + } +} + +export default ExternProcessor; diff --git a/backend/src/services/learning-objects/processing/extern/extern_processor.js b/backend/src/services/learning-objects/processing/extern/extern_processor.js deleted file mode 100644 index b89ec401..00000000 --- a/backend/src/services/learning-objects/processing/extern/extern_processor.js +++ /dev/null @@ -1,37 +0,0 @@ -import Processor from "../processor.ts"; -import { isValidHttpUrl } from '../../utils/utils.js' -import InvalidArgumentError from '../../utils/invalid_argument_error.js' -import DOMPurify from 'isomorphic-dompurify'; -import Logger from "../../logger.js"; - -let logger = Logger.getLogger() -class ExternProcessor extends Processor { - constructor() { - super(); - } - - /** - * - * @param {string} externURL - * @param {object} args Optional arguments specific to the render function of the ExternProcessor - * @returns - */ - render(externURL, {height = 315, aspect_ratio = 'iframe-16-9'} = {}) { - if (!isValidHttpUrl(externURL)) { - throw new InvalidArgumentError("The url is not valid: " + externURL); - } - - // If a seperate youtube-processor would be added, this code would need to move to that processor - // Converts youtube urls to youtube-embed urls - let match = /(.*youtube.com\/)watch\?v=(.*)/.exec(externURL) - if (match) { - externURL = match[1] + "embed/" + match[2]; - } - - - return DOMPurify.sanitize(`
`, { ADD_TAGS: ["iframe"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling']}); - - } -} - -export default ExternProcessor; diff --git a/backend/src/services/learning-objects/processing/gift/gift-processor.ts b/backend/src/services/learning-objects/processing/gift/gift-processor.ts new file mode 100644 index 00000000..1f117d59 --- /dev/null +++ b/backend/src/services/learning-objects/processing/gift/gift-processor.ts @@ -0,0 +1,58 @@ +/** + * 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"; +import {GIFTQuestionRenderer} from "./question-renderers/gift-question-renderer"; +import {MultipleChoiceQuestionRenderer} from "./question-renderers/multiple-choice-question-renderer"; +import {CategoryQuestionRenderer} from "./question-renderers/category-question-renderer"; +import {DescriptionQuestionRenderer} from "./question-renderers/description-question-renderer"; +import {EssayQuestionRenderer} from "./question-renderers/essay-question-renderer"; +import {MatchingQuestionRenderer} from "./question-renderers/matching-question-renderer"; +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"; + +class GiftProcessor extends Processor { + + private renderers: RendererMap = { + Category: new CategoryQuestionRenderer(), + Description: new DescriptionQuestionRenderer(), + Essay: new EssayQuestionRenderer(), + Matching: new MatchingQuestionRenderer(), + Numerical: new NumericalQuestionRenderer(), + Short: new ShortQuestionRenderer(), + TF: new TrueFalseQuestionRenderer(), + MC: new MultipleChoiceQuestionRenderer() + } + + constructor() { + super(DwengoContentType.GIFT); + } + + override renderFn(giftString: string) { + const quizQuestions: GIFTQuestion[] = parse(giftString); + + let html = "
"; + for (let question of quizQuestions) { + html += this.renderQuestion(question); + } + html += "
" + + return DOMPurify.sanitize(html); + } + + private renderQuestion(question: T): string { + const renderer = this.renderers[question.type] as GIFTQuestionRenderer; + return renderer.render(question); + } +} + +type RendererMap = { + [K in GIFTQuestion["type"]]: GIFTQuestionRenderer> +}; + +export default GiftProcessor; diff --git a/backend/src/services/learning-objects/processing/gift/gift_processor.js b/backend/src/services/learning-objects/processing/gift/gift_processor.js deleted file mode 100644 index 4825eb75..00000000 --- a/backend/src/services/learning-objects/processing/gift/gift_processor.js +++ /dev/null @@ -1,52 +0,0 @@ -import Processor from "../processor.ts"; -import { isValidHttpUrl } from '../../utils/utils.js' -import { findFile } from '../../utils/file_io.js' -import InvalidArgumentError from '../../utils/invalid_argument_error.js' -import DOMPurify from 'isomorphic-dompurify'; -import ProcessingHistory from "../../models/processing_history.js"; -import path from "path" -import fs from "fs" -import { parse } from "gift-pegjs" - -class GiftProcessor extends Processor { - - constructor() { - super(); - this.types = ["text/gift"] - } - - /** - * - * @param {string} audioUrl - * @param {object} args Optional arguments specific to the render function of the GiftProcessor - * @returns - */ - render(giftString, args = { }) { - - const quizQuestions = parse(giftString); - console.log(quizQuestions); - - - return DOMPurify.sanitize(`