Merge remote-tracking branch 'origin/feature/own-learning-objects' into feature/own-learning-objects
# Conflicts: # backend/src/services/learning-objects/database-learning-object-provider.ts # backend/tests/services/learning-objects/database-learning-object-provider.test.ts # backend/tests/test-utils/expectations.ts
This commit is contained in:
		
						commit
						9f28e4ed17
					
				
					 84 changed files with 874 additions and 1048 deletions
				
			
		|  | @ -1,4 +1,4 @@ | |||
| import {EnvVars, getEnvVar} from "./util/envvars"; | ||||
| import { EnvVars, getEnvVar } from './util/envvars'; | ||||
| 
 | ||||
| // API
 | ||||
| export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); | ||||
|  |  | |||
|  | @ -1,38 +1,35 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import {FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier} from '../interfaces/learning-content'; | ||||
| import learningObjectService from "../services/learning-objects/learning-object-service"; | ||||
| 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"; | ||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content'; | ||||
| import learningObjectService from '../services/learning-objects/learning-object-service'; | ||||
| 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) { | ||||
|         throw new BadRequestException("HRUID is required."); | ||||
|         throw new BadRequestException('HRUID is required.'); | ||||
|     } | ||||
|     return { | ||||
|         hruid: req.params.hruid as string, | ||||
|         language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language, | ||||
|         version: parseInt(req.query.version as string) | ||||
|         version: parseInt(req.query.version as string), | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentifier { | ||||
|     if (!req.query.hruid) { | ||||
|         throw new BadRequestException("HRUID is required."); | ||||
|         throw new BadRequestException('HRUID is required.'); | ||||
|     } | ||||
|     return { | ||||
|         hruid: req.params.hruid as string, | ||||
|         language: (req.query.language as Language) || FALLBACK_LANG | ||||
|     } | ||||
|         language: (req.query.language as Language) || FALLBACK_LANG, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export async function getAllLearningObjects( | ||||
|     req: Request, | ||||
|     res: Response | ||||
| ): Promise<void> { | ||||
| export async function getAllLearningObjects(req: Request, res: Response): Promise<void> { | ||||
|     const learningPathId = getLearningPathIdentifierFromRequest(req); | ||||
|     const full = req.query.full; | ||||
| 
 | ||||
|  | @ -46,10 +43,7 @@ export async function getAllLearningObjects( | |||
|     res.json(learningObjects); | ||||
| } | ||||
| 
 | ||||
| export async function getLearningObject( | ||||
|     req: Request, | ||||
|     res: Response | ||||
| ): Promise<void> { | ||||
| export async function getLearningObject(req: Request, res: Response): Promise<void> { | ||||
|     const learningObjectId = getLearningObjectIdentifierFromRequest(req); | ||||
| 
 | ||||
|     const learningObject = await learningObjectService.getLearningObjectById(learningObjectId); | ||||
|  | @ -71,5 +65,5 @@ export async function getAttachment(req: Request, res: Response): Promise<void> | |||
|     if (!attachment) { | ||||
|         throw new NotFoundError(`Attachment ${name} not found`); | ||||
|     } | ||||
|     res.setHeader("Content-Type", attachment.mimeType).send(attachment.content) | ||||
|     res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); | ||||
| } | ||||
|  |  | |||
|  | @ -1,16 +1,13 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { themes } from '../data/themes.js'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import learningPathService from "../services/learning-paths/learning-path-service"; | ||||
| import {NotFoundException} from "../exceptions"; | ||||
| import learningPathService from '../services/learning-paths/learning-path-service'; | ||||
| import { NotFoundException } from '../exceptions'; | ||||
| 
 | ||||
| /** | ||||
|  * Fetch learning paths based on query parameters. | ||||
|  */ | ||||
| export async function getLearningPaths( | ||||
|     req: Request, | ||||
|     res: Response | ||||
| ): Promise<void> { | ||||
| export async function getLearningPaths(req: Request, res: Response): Promise<void> { | ||||
|     const hruids = req.query.hruid; | ||||
|     const themeKey = req.query.theme as string; | ||||
|     const searchQuery = req.query.search as string; | ||||
|  | @ -19,35 +16,22 @@ export async function getLearningPaths( | |||
|     let hruidList; | ||||
| 
 | ||||
|     if (hruids) { | ||||
|         hruidList = Array.isArray(hruids) | ||||
|             ? hruids.map(String) | ||||
|             : [String(hruids)]; | ||||
|         hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)]; | ||||
|     } else if (themeKey) { | ||||
|         const theme = themes.find((t) => { | ||||
|             return t.title === themeKey; | ||||
|         }); | ||||
|         const theme = themes.find((t) => t.title === themeKey); | ||||
|         if (theme) { | ||||
|             hruidList = theme.hruids; | ||||
|         } else { | ||||
|             throw new NotFoundException(`Theme "${themeKey}" not found.`); | ||||
|         } | ||||
|     } else if (searchQuery) { | ||||
|         const searchResults = await learningPathService.searchLearningPaths( | ||||
|             searchQuery, | ||||
|             language | ||||
|         ); | ||||
|         const searchResults = await learningPathService.searchLearningPaths(searchQuery, language); | ||||
|         res.json(searchResults); | ||||
|         return; | ||||
|     } else { | ||||
|         hruidList = themes.flatMap((theme) => { | ||||
|             return theme.hruids; | ||||
|         }); | ||||
|         hruidList = themes.flatMap((theme) => theme.hruids); | ||||
|     } | ||||
| 
 | ||||
|     const learningPaths = await learningPathService.fetchLearningPaths( | ||||
|         hruidList, | ||||
|         language, | ||||
|         `HRUIDs: ${hruidList.join(', ')}` | ||||
|     ); | ||||
|     const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language, `HRUIDs: ${hruidList.join(', ')}`); | ||||
|     res.json(learningPaths.data); | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| 
 | ||||
| import { Request, Response } from 'express'; | ||||
| import { themes } from '../data/themes.js'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
|  |  | |||
|  | @ -1,13 +1,10 @@ | |||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||
| import { Attachment } from '../../entities/content/attachment.entity.js'; | ||||
| import {Language} from "../../entities/content/language"; | ||||
| import {LearningObjectIdentifier} from "../../entities/content/learning-object-identifier"; | ||||
| import { Language } from '../../entities/content/language'; | ||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier'; | ||||
| 
 | ||||
| export class AttachmentRepository extends DwengoEntityRepository<Attachment> { | ||||
|     public findByLearningObjectIdAndName( | ||||
|         learningObjectId: LearningObjectIdentifier, | ||||
|         name: string | ||||
|     ): Promise<Attachment | null> { | ||||
|     public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> { | ||||
|         return this.findOne({ | ||||
|             learningObject: { | ||||
|                 hruid: learningObjectId.hruid, | ||||
|  | @ -18,24 +15,23 @@ export class AttachmentRepository extends DwengoEntityRepository<Attachment> { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public findByMostRecentVersionOfLearningObjectAndName( | ||||
|         hruid: string, | ||||
|         language: Language, | ||||
|         attachmentName: string | ||||
|     ): Promise<Attachment | null> { | ||||
|         return this.findOne({ | ||||
|             learningObject: { | ||||
|                 hruid: hruid, | ||||
|                 language: language | ||||
|             }, | ||||
|             name: attachmentName | ||||
|         }, { | ||||
|             orderBy: { | ||||
|     public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise<Attachment | null> { | ||||
|         return this.findOne( | ||||
|             { | ||||
|                 learningObject: { | ||||
|                     version: 'DESC' | ||||
|                 } | ||||
|                     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.
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,12 +1,10 @@ | |||
| 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"; | ||||
| import { Language } from '../../entities/content/language'; | ||||
| 
 | ||||
| export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { | ||||
|     public findByIdentifier( | ||||
|         identifier: LearningObjectIdentifier | ||||
|     ): Promise<LearningObject | null> { | ||||
|     public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { | ||||
|         return this.findOne( | ||||
|             { | ||||
|                 hruid: identifier.hruid, | ||||
|  | @ -14,7 +12,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj | |||
|                 version: identifier.version, | ||||
|             }, | ||||
|             { | ||||
|                 populate: ["keywords"] | ||||
|                 populate: ['keywords'], | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
|  | @ -23,13 +21,13 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj | |||
|         return this.findOne( | ||||
|             { | ||||
|                 hruid: hruid, | ||||
|                 language: language | ||||
|                 language: language, | ||||
|             }, | ||||
|             { | ||||
|                 populate: ["keywords"], | ||||
|                 populate: ['keywords'], | ||||
|                 orderBy: { | ||||
|                     version: "DESC" | ||||
|                 } | ||||
|                     version: 'DESC', | ||||
|                 }, | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
|  |  | |||
|  | @ -1,16 +1,10 @@ | |||
| import {DwengoEntityRepository} from '../dwengo-entity-repository.js'; | ||||
| import {LearningPath} from '../../entities/content/learning-path.entity.js'; | ||||
| import {Language} from '../../entities/content/language.js'; | ||||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||
| import { LearningPath } from '../../entities/content/learning-path.entity.js'; | ||||
| import { Language } from '../../entities/content/language.js'; | ||||
| 
 | ||||
| export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | ||||
|     public findByHruidAndLanguage( | ||||
|         hruid: string, | ||||
|         language: Language | ||||
|     ): Promise<LearningPath | null> { | ||||
|         return this.findOne( | ||||
|             { hruid: hruid, language: language }, | ||||
|             { populate: ["nodes", "nodes.transitions"] } | ||||
|         ); | ||||
|     public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { | ||||
|         return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -24,12 +18,9 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath> | |||
|         return this.findAll({ | ||||
|             where: { | ||||
|                 language: language, | ||||
|                 $or: [ | ||||
|                     { title: { $like: `%${query}%`} }, | ||||
|                     { description: { $like: `%${query}%`} } | ||||
|                 ] | ||||
|                 $or: [{ title: { $like: `%${query}%` } }, { description: { $like: `%${query}%` } }], | ||||
|             }, | ||||
|             populate: ["nodes", "nodes.transitions"] | ||||
|             populate: ['nodes', 'nodes.transitions'], | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -28,8 +28,8 @@ import { LearningPath } from '../entities/content/learning-path.entity.js'; | |||
| import { LearningPathRepository } from './content/learning-path-repository.js'; | ||||
| import { AttachmentRepository } from './content/attachment-repository.js'; | ||||
| import { Attachment } from '../entities/content/attachment.entity.js'; | ||||
| import {LearningPathNode} from "../entities/content/learning-path-node.entity"; | ||||
| import {LearningPathTransition} from "../entities/content/learning-path-transition.entity"; | ||||
| import { LearningPathNode } from '../entities/content/learning-path-node.entity'; | ||||
| import { LearningPathTransition } from '../entities/content/learning-path-transition.entity'; | ||||
| 
 | ||||
| let entityManager: EntityManager | undefined; | ||||
| 
 | ||||
|  | @ -73,17 +73,8 @@ export const getQuestionRepository = repositoryGetter<Question, QuestionReposito | |||
| export const getAnswerRepository = repositoryGetter<Answer, AnswerRepository>(Answer); | ||||
| 
 | ||||
| /* Learning content */ | ||||
| export const getLearningObjectRepository = repositoryGetter< | ||||
|     LearningObject, | ||||
|     LearningObjectRepository | ||||
| >(LearningObject); | ||||
| export const getLearningPathRepository = repositoryGetter< | ||||
|     LearningPath, | ||||
|     LearningPathRepository | ||||
| >(LearningPath); | ||||
| export const getLearningObjectRepository = repositoryGetter<LearningObject, LearningObjectRepository>(LearningObject); | ||||
| export const getLearningPathRepository = repositoryGetter<LearningPath, LearningPathRepository>(LearningPath); | ||||
| export const getLearningPathNodeRepository = repositoryGetter(LearningPathNode); | ||||
| export const getLearningPathTransitionRepository = repositoryGetter(LearningPathTransition); | ||||
| export const getAttachmentRepository = repositoryGetter< | ||||
|     Attachment, | ||||
|     AttachmentRepository | ||||
| >(Attachment); | ||||
| export const getAttachmentRepository = repositoryGetter<Attachment, AttachmentRepository>(Attachment); | ||||
|  |  | |||
|  | @ -2,9 +2,9 @@ import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro | |||
| import { Class } from '../classes/class.entity.js'; | ||||
| import { Group } from './group.entity.js'; | ||||
| import { Language } from '../content/language.js'; | ||||
| import {AssignmentRepository} from "../../data/assignments/assignment-repository"; | ||||
| import { AssignmentRepository } from '../../data/assignments/assignment-repository'; | ||||
| 
 | ||||
| @Entity({repository: () => AssignmentRepository}) | ||||
| @Entity({ repository: () => AssignmentRepository }) | ||||
| export class Assignment { | ||||
|     @ManyToOne({ entity: () => Class, primary: true }) | ||||
|     within!: Class; | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; | ||||
| import { Assignment } from './assignment.entity.js'; | ||||
| import { Student } from '../users/student.entity.js'; | ||||
| import {GroupRepository} from "../../data/assignments/group-repository"; | ||||
| import { GroupRepository } from '../../data/assignments/group-repository'; | ||||
| 
 | ||||
| @Entity({repository: () => GroupRepository}) | ||||
| @Entity({ repository: () => GroupRepository }) | ||||
| export class Group { | ||||
|     @ManyToOne({ | ||||
|         entity: () => Assignment, | ||||
|  |  | |||
|  | @ -2,9 +2,9 @@ import { Student } from '../users/student.entity.js'; | |||
| import { Group } from './group.entity.js'; | ||||
| import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Language } from '../content/language.js'; | ||||
| import {SubmissionRepository} from "../../data/assignments/submission-repository"; | ||||
| import { SubmissionRepository } from '../../data/assignments/submission-repository'; | ||||
| 
 | ||||
| @Entity({repository: () => SubmissionRepository}) | ||||
| @Entity({ repository: () => SubmissionRepository }) | ||||
| export class Submission { | ||||
|     @PrimaryKey({ type: 'string' }) | ||||
|     learningObjectHruid!: string; | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { Entity, Enum, ManyToOne } from '@mikro-orm/core'; | ||||
| import { Student } from '../users/student.entity.js'; | ||||
| import { Class } from './class.entity.js'; | ||||
| import {ClassJoinRequestRepository} from "../../data/classes/class-join-request-repository"; | ||||
| import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository'; | ||||
| 
 | ||||
| @Entity({repository: () => ClassJoinRequestRepository}) | ||||
| @Entity({ repository: () => ClassJoinRequestRepository }) | ||||
| export class ClassJoinRequest { | ||||
|     @ManyToOne({ | ||||
|         entity: () => Student, | ||||
|  |  | |||
|  | @ -2,9 +2,9 @@ import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm | |||
| import { v4 } from 'uuid'; | ||||
| import { Teacher } from '../users/teacher.entity.js'; | ||||
| import { Student } from '../users/student.entity.js'; | ||||
| import {ClassRepository} from "../../data/classes/class-repository"; | ||||
| import { ClassRepository } from '../../data/classes/class-repository'; | ||||
| 
 | ||||
| @Entity({repository: () => ClassRepository}) | ||||
| @Entity({ repository: () => ClassRepository }) | ||||
| export class Class { | ||||
|     @PrimaryKey() | ||||
|     classId = v4(); | ||||
|  |  | |||
|  | @ -1,12 +1,12 @@ | |||
| import { Entity, ManyToOne } from '@mikro-orm/core'; | ||||
| import { Teacher } from '../users/teacher.entity.js'; | ||||
| import { Class } from './class.entity.js'; | ||||
| import {TeacherInvitationRepository} from "../../data/classes/teacher-invitation-repository"; | ||||
| import { TeacherInvitationRepository } from '../../data/classes/teacher-invitation-repository'; | ||||
| 
 | ||||
| /** | ||||
|  * Invitation of a teacher into a class (in order to teach it). | ||||
|  */ | ||||
| @Entity({repository: () => TeacherInvitationRepository}) | ||||
| @Entity({ repository: () => TeacherInvitationRepository }) | ||||
| export class TeacherInvitation { | ||||
|     @ManyToOne({ | ||||
|         entity: () => Teacher, | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { LearningObject } from './learning-object.entity.js'; | ||||
| import {AttachmentRepository} from "../../data/content/attachment-repository"; | ||||
| import { AttachmentRepository } from '../../data/content/attachment-repository'; | ||||
| 
 | ||||
| @Entity({repository: () => AttachmentRepository}) | ||||
| @Entity({ repository: () => AttachmentRepository }) | ||||
| export class Attachment { | ||||
|     @ManyToOne({ | ||||
|         entity: () => LearningObject, | ||||
|  |  | |||
|  | @ -182,5 +182,5 @@ export enum Language { | |||
|     Yiddish = 'yi', | ||||
|     Yoruba = 'yo', | ||||
|     Zhuang = 'za', | ||||
|     Zulu = 'zu' | ||||
|     Zulu = 'zu', | ||||
| } | ||||
|  |  | |||
|  | @ -2,11 +2,11 @@ import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, | |||
| 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 {v4} from "uuid"; | ||||
| import {LearningObjectRepository} from "../../data/content/learning-object-repository"; | ||||
| import { DwengoContentType } from '../../services/learning-objects/processing/content-type'; | ||||
| import { v4 } from 'uuid'; | ||||
| import { LearningObjectRepository } from '../../data/content/learning-object-repository'; | ||||
| 
 | ||||
| @Entity({repository: () => LearningObjectRepository}) | ||||
| @Entity({ repository: () => LearningObjectRepository }) | ||||
| export class LearningObject { | ||||
|     @PrimaryKey({ type: 'string' }) | ||||
|     hruid!: string; | ||||
|  | @ -20,7 +20,7 @@ export class LearningObject { | |||
|     @PrimaryKey({ type: 'number' }) | ||||
|     version: number = 1; | ||||
| 
 | ||||
|     @Property({type: 'uuid', unique: true}) | ||||
|     @Property({ type: 'uuid', unique: true }) | ||||
|     uuid = v4(); | ||||
| 
 | ||||
|     @ManyToMany({ | ||||
|  |  | |||
|  | @ -1,15 +1,14 @@ | |||
| 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"; | ||||
| 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: "integer", autoincrement: true }) | ||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||
|     nodeNumber!: number; | ||||
| 
 | ||||
|     @Property({ type: 'string' }) | ||||
|  | @ -27,7 +26,7 @@ export class LearningPathNode { | |||
|     @Property({ type: 'bool' }) | ||||
|     startNode!: boolean; | ||||
| 
 | ||||
|     @OneToMany({ entity: () => LearningPathTransition, mappedBy: "node" }) | ||||
|     @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' }) | ||||
|     transitions: LearningPathTransition[] = []; | ||||
| 
 | ||||
|     @Property({ length: 3 }) | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import {Entity, ManyToOne, PrimaryKey, Property} from "@mikro-orm/core"; | ||||
| import {LearningPathNode} from "./learning-path-node.entity"; | ||||
| import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { LearningPathNode } from './learning-path-node.entity'; | ||||
| 
 | ||||
| @Entity() | ||||
| export class LearningPathTransition { | ||||
|     @ManyToOne({entity: () => LearningPathNode, primary: true }) | ||||
|     @ManyToOne({ entity: () => LearningPathNode, primary: true }) | ||||
|     node!: LearningPathNode; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'numeric' }) | ||||
|  |  | |||
|  | @ -1,16 +1,10 @@ | |||
| import { | ||||
|     Entity, | ||||
|     Enum, | ||||
|     ManyToMany, OneToMany, | ||||
|     PrimaryKey, | ||||
|     Property, | ||||
| } from '@mikro-orm/core'; | ||||
| import { Entity, Enum, 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"; | ||||
| import { LearningPathRepository } from '../../data/content/learning-path-repository'; | ||||
| import { LearningPathNode } from './learning-path-node.entity'; | ||||
| 
 | ||||
| @Entity({repository: () => LearningPathRepository}) | ||||
| @Entity({ repository: () => LearningPathRepository }) | ||||
| export class LearningPath { | ||||
|     @PrimaryKey({ type: 'string' }) | ||||
|     hruid!: string; | ||||
|  | @ -30,6 +24,6 @@ export class LearningPath { | |||
|     @Property({ type: 'blob', nullable: true }) | ||||
|     image: Buffer | null = null; | ||||
| 
 | ||||
|     @OneToMany({ entity: () => LearningPathNode, mappedBy: "learningPath" }) | ||||
|     @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' }) | ||||
|     nodes: LearningPathNode[] = []; | ||||
| } | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Question } from './question.entity.js'; | ||||
| import { Teacher } from '../users/teacher.entity.js'; | ||||
| import {AnswerRepository} from "../../data/questions/answer-repository"; | ||||
| import { AnswerRepository } from '../../data/questions/answer-repository'; | ||||
| 
 | ||||
| @Entity({repository: () => AnswerRepository}) | ||||
| @Entity({ repository: () => AnswerRepository }) | ||||
| export class Answer { | ||||
|     @ManyToOne({ | ||||
|         entity: () => Teacher, | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Language } from '../content/language.js'; | ||||
| import { Student } from '../users/student.entity.js'; | ||||
| import {QuestionRepository} from "../../data/questions/question-repository"; | ||||
| import { QuestionRepository } from '../../data/questions/question-repository'; | ||||
| 
 | ||||
| @Entity({repository: () => QuestionRepository}) | ||||
| @Entity({ repository: () => QuestionRepository }) | ||||
| export class Question { | ||||
|     @PrimaryKey({ type: 'string' }) | ||||
|     learningObjectHruid!: string; | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { Collection, Entity, ManyToMany } from '@mikro-orm/core'; | ||||
| import { User } from './user.entity.js'; | ||||
| import { Class } from '../classes/class.entity.js'; | ||||
| import {TeacherRepository} from "../../data/users/teacher-repository"; | ||||
| import { TeacherRepository } from '../../data/users/teacher-repository'; | ||||
| 
 | ||||
| @Entity({repository: () => TeacherRepository}) | ||||
| @Entity({ repository: () => TeacherRepository }) | ||||
| export class Teacher extends User { | ||||
|     @ManyToMany(() => Class) | ||||
|     classes!: Collection<Class>; | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {Language} from "../entities/content/language"; | ||||
| import { Language } from '../entities/content/language'; | ||||
| 
 | ||||
| export interface Transition { | ||||
|     default: boolean; | ||||
|  |  | |||
|  | @ -1,8 +1,5 @@ | |||
| import express from 'express'; | ||||
| import { | ||||
|     getAllLearningObjects, getAttachment, | ||||
|     getLearningObject, getLearningObjectHTML, | ||||
| } from '../controllers/learning-objects.js'; | ||||
| import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js'; | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,21 +1,23 @@ | |||
| import {getAttachmentRepository} from "../../data/repositories"; | ||||
| import {Attachment} from "../../entities/content/attachment.entity"; | ||||
| import {LearningObjectIdentifier} from "../../interfaces/learning-content"; | ||||
| import { getAttachmentRepository } from '../../data/repositories'; | ||||
| import { Attachment } from '../../entities/content/attachment.entity'; | ||||
| import { LearningObjectIdentifier } from '../../interfaces/learning-content'; | ||||
| 
 | ||||
| const attachmentService = { | ||||
|     getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> { | ||||
|         const attachmentRepo = getAttachmentRepository(); | ||||
| 
 | ||||
|         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); | ||||
|             return attachmentRepo.findByLearningObjectIdAndName( | ||||
|                 { | ||||
|                     hruid: learningObjectId.hruid, | ||||
|                     language: learningObjectId.language, | ||||
|                     version: learningObjectId.version, | ||||
|                 }, | ||||
|                 attachmentName | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|         return attachmentRepo.findByMostRecentVersionOfLearningObjectAndName(learningObjectId.hruid, learningObjectId.language, attachmentName); | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export default attachmentService; | ||||
|  |  | |||
|  | @ -35,20 +35,18 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL | |||
|         educationalGoals: learningObject.educationalGoals, | ||||
|         returnValue: { | ||||
|             callback_url: learningObject.returnValue.callbackUrl, | ||||
|             callback_schema: JSON.parse(learningObject.returnValue.callbackSchema) | ||||
|             callback_schema: JSON.parse(learningObject.returnValue.callbackSchema), | ||||
|         }, | ||||
|         skosConcepts: learningObject.skosConcepts, | ||||
|         targetAges: learningObject.targetAges || [], | ||||
|         teacherExclusive: learningObject.teacherExclusive | ||||
|     } | ||||
|         teacherExclusive: learningObject.teacherExclusive, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> { | ||||
|     const learningObjectRepo = getLearningObjectRepository(); | ||||
| 
 | ||||
|     return learningObjectRepo.findLatestByHruidAndLanguage( | ||||
|         id.hruid, id.language as Language | ||||
|     ); | ||||
|     return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -69,16 +67,11 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | |||
|     async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { | ||||
|         const learningObjectRepo = getLearningObjectRepository(); | ||||
| 
 | ||||
|         const learningObject  = await learningObjectRepo.findLatestByHruidAndLanguage( | ||||
|             id.hruid, id.language as Language | ||||
|         ); | ||||
|         const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); | ||||
|         if (!learningObject) { | ||||
|             return null; | ||||
|         } | ||||
|         return await processingService.render( | ||||
|             learningObject, | ||||
|             (id) => findLearningObjectEntityById(id) | ||||
|         ); | ||||
|         return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id)); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|  | @ -89,9 +82,9 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | |||
| 
 | ||||
|         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."); | ||||
|             throw new NotFoundError('The learning path with the given ID could not be found.'); | ||||
|         } | ||||
|         return learningPath.nodes.map(it => it.learningObjectHruid); | ||||
|         return learningPath.nodes.map((it) => it.learningObjectHruid); // TODO: Determine this based on the submissions of the user.
 | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|  | @ -102,15 +95,15 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | |||
| 
 | ||||
|         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."); | ||||
|             throw new NotFoundError('The learning path with the given ID could not be found.'); | ||||
|         } | ||||
|         const learningObjects = await Promise.all( | ||||
|             learningPath.nodes.map(it => { | ||||
|             learningPath.nodes.map((it) => { | ||||
|                 const learningObject = learningObjectService.getLearningObjectById({ | ||||
|                     hruid: it.learningObjectHruid, | ||||
|                     language: it.language, | ||||
|                     version: it.version | ||||
|                 }) | ||||
|                     version: it.version, | ||||
|                 }); | ||||
|                 if (learningObject === null) { | ||||
|                     console.log(`WARN: Learning object corresponding with node ${it} not found!`); | ||||
|                 } | ||||
|  | @ -119,6 +112,6 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | |||
|         ); | ||||
|         return learningObjects.filter(it => it !== null); | ||||
|     } | ||||
| } | ||||
| }; | ||||
| 
 | ||||
| export default databaseLearningObjectProvider; | ||||
|  |  | |||
|  | @ -1,22 +1,22 @@ | |||
| import { DWENGO_API_BASE } from '../../config.js'; | ||||
| import { fetchWithLogging } from '../../util/apiHelper.js'; | ||||
| import { | ||||
|     FilteredLearningObject, LearningObjectIdentifier, | ||||
|     FilteredLearningObject, | ||||
|     LearningObjectIdentifier, | ||||
|     LearningObjectMetadata, | ||||
|     LearningObjectNode, LearningPathIdentifier, | ||||
|     LearningObjectNode, | ||||
|     LearningPathIdentifier, | ||||
|     LearningPathResponse, | ||||
| } from '../../interfaces/learning-content.js'; | ||||
| import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; | ||||
| import {LearningObjectProvider} from "./learning-object-provider"; | ||||
| import { LearningObjectProvider } from './learning-object-provider'; | ||||
| 
 | ||||
| /** | ||||
|  * Helper function to convert the learning object metadata retrieved from the API to a FilteredLearningObject which | ||||
|  * our API should return. | ||||
|  * @param data | ||||
|  */ | ||||
| function filterData( | ||||
|     data: LearningObjectMetadata | ||||
| ): FilteredLearningObject { | ||||
| function filterData(data: LearningObjectMetadata): FilteredLearningObject { | ||||
|     return { | ||||
|         key: data.hruid, // Hruid learningObject (not path)
 | ||||
|         _id: data._id, | ||||
|  | @ -43,48 +43,33 @@ function filterData( | |||
| /** | ||||
|  * Generic helper function to fetch all learning objects from a given path (full data or just HRUIDs) | ||||
|  */ | ||||
| async function fetchLearningObjects( | ||||
|     learningPathId: LearningPathIdentifier, | ||||
|     full: boolean | ||||
| ): Promise<FilteredLearningObject[] | string[]> { | ||||
| async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full: boolean): Promise<FilteredLearningObject[] | string[]> { | ||||
|     try { | ||||
|         const learningPathResponse: LearningPathResponse = | ||||
|             await dwengoApiLearningPathProvider.fetchLearningPaths( | ||||
|                 [learningPathId.hruid], | ||||
|                 learningPathId.language, | ||||
|                 `Learning path for HRUID "${learningPathId.hruid}"` | ||||
|             ); | ||||
|         const learningPathResponse: LearningPathResponse = await dwengoApiLearningPathProvider.fetchLearningPaths( | ||||
|             [learningPathId.hruid], | ||||
|             learningPathId.language, | ||||
|             `Learning path for HRUID "${learningPathId.hruid}"` | ||||
|         ); | ||||
| 
 | ||||
|         if ( | ||||
|             !learningPathResponse.success || | ||||
|             !learningPathResponse.data?.length | ||||
|         ) { | ||||
|             console.error( | ||||
|                 `⚠️ WARNING: Learning path "${learningPathId.hruid}" exists but contains no learning objects.` | ||||
|             ); | ||||
|         if (!learningPathResponse.success || !learningPathResponse.data?.length) { | ||||
|             console.error(`⚠️ WARNING: Learning path "${learningPathId.hruid}" exists but contains no learning objects.`); | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         const nodes: LearningObjectNode[] = learningPathResponse.data[0].nodes; | ||||
| 
 | ||||
|         if (!full) { | ||||
|             return nodes.map((node) => { | ||||
|                 return node.learningobject_hruid; | ||||
|             }); | ||||
|             return nodes.map((node) => node.learningobject_hruid); | ||||
|         } | ||||
| 
 | ||||
|         return await Promise.all( | ||||
|             nodes.map(async (node) => { | ||||
|                 return dwengoApiLearningObjectProvider.getLearningObjectById({ | ||||
|             nodes.map(async (node) => | ||||
|                 dwengoApiLearningObjectProvider.getLearningObjectById({ | ||||
|                     hruid: node.learningobject_hruid, | ||||
|                     language: learningPathId.language | ||||
|                 }); | ||||
|             }) | ||||
|         ).then((objects) => { | ||||
|             return objects.filter((obj): obj is FilteredLearningObject => { | ||||
|                 return obj !== null; | ||||
|             }); | ||||
|         }); | ||||
|                     language: learningPathId.language, | ||||
|                 }) | ||||
|             ) | ||||
|         ).then((objects) => objects.filter((obj): obj is FilteredLearningObject => obj !== null)); | ||||
|     } catch (error) { | ||||
|         console.error('❌ Error fetching learning objects:', error); | ||||
|         return []; | ||||
|  | @ -95,19 +80,17 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | |||
|     /** | ||||
|      * Fetches a single learning object by its HRUID | ||||
|      */ | ||||
|     async getLearningObjectById( | ||||
|         id: LearningObjectIdentifier | ||||
|     ): Promise<FilteredLearningObject | null> { | ||||
|         let metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; | ||||
|     async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { | ||||
|         const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; | ||||
|         const metadata = await fetchWithLogging<LearningObjectMetadata>( | ||||
|             metadataUrl, | ||||
|             `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, | ||||
|             { | ||||
|                 params: id | ||||
|                 params: id, | ||||
|             } | ||||
|         ); | ||||
| 
 | ||||
|         if (!metadata || typeof metadata !== "object") { | ||||
|         if (!metadata || typeof metadata !== 'object') { | ||||
|             console.error(`⚠️ WARNING: Learning object "${id.hruid}" not found.`); | ||||
|             return null; | ||||
|         } | ||||
|  | @ -119,10 +102,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | |||
|      * Fetch full learning object data (metadata) | ||||
|      */ | ||||
|     async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> { | ||||
|         return (await fetchLearningObjects( | ||||
|             id, | ||||
|             true, | ||||
|         )) as FilteredLearningObject[]; | ||||
|         return (await fetchLearningObjects(id, true)) as FilteredLearningObject[]; | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|  | @ -138,13 +118,9 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | |||
|      */ | ||||
|     async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { | ||||
|         const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; | ||||
|         const html = await fetchWithLogging<string>( | ||||
|             htmlUrl, | ||||
|             `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, | ||||
|             { | ||||
|                 params: id | ||||
|             } | ||||
|         ); | ||||
|         const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { | ||||
|             params: id, | ||||
|         }); | ||||
| 
 | ||||
|         if (!html) { | ||||
|             console.error(`⚠️ WARNING: Learning object "${id.hruid}" not found.`); | ||||
|  | @ -152,7 +128,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | |||
|         } | ||||
| 
 | ||||
|         return html; | ||||
|     } | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export default dwengoApiLearningObjectProvider; | ||||
|  |  | |||
|  | @ -1,8 +1,4 @@ | |||
| import { | ||||
|     FilteredLearningObject, | ||||
|     LearningObjectIdentifier, | ||||
|     LearningPathIdentifier | ||||
| } from "../../interfaces/learning-content"; | ||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content'; | ||||
| 
 | ||||
| export interface LearningObjectProvider { | ||||
|     /** | ||||
|  |  | |||
|  | @ -1,19 +1,14 @@ | |||
| import { | ||||
|     FilteredLearningObject, | ||||
|     LearningObjectIdentifier, | ||||
|     LearningPathIdentifier | ||||
| } from "../../interfaces/learning-content"; | ||||
| import dwengoApiLearningObjectProvider from "./dwengo-api-learning-object-provider"; | ||||
| import {LearningObjectProvider} from "./learning-object-provider"; | ||||
| import {EnvVars, getEnvVar} from "../../util/envvars"; | ||||
| import databaseLearningObjectProvider from "./database-learning-object-provider"; | ||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content'; | ||||
| import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider'; | ||||
| import { LearningObjectProvider } from './learning-object-provider'; | ||||
| import { EnvVars, getEnvVar } from '../../util/envvars'; | ||||
| import databaseLearningObjectProvider from './database-learning-object-provider'; | ||||
| 
 | ||||
| function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { | ||||
|     if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) { | ||||
|         return databaseLearningObjectProvider; | ||||
|     } else { | ||||
|         return dwengoApiLearningObjectProvider; | ||||
|     } | ||||
|     return dwengoApiLearningObjectProvider; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -46,7 +41,7 @@ const learningObjectService = { | |||
|      */ | ||||
|     getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { | ||||
|         return getProvider(id).getLearningObjectHTML(id); | ||||
|     } | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export default learningObjectService; | ||||
|  |  | |||
|  | @ -5,12 +5,11 @@ | |||
|  */ | ||||
| 
 | ||||
| import DOMPurify from 'isomorphic-dompurify'; | ||||
| import {type} from "node:os"; | ||||
| import {DwengoContentType} from "../content-type"; | ||||
| import {StringProcessor} from "../string-processor"; | ||||
| import { type } from 'node:os'; | ||||
| import { DwengoContentType } from '../content-type'; | ||||
| import { StringProcessor } from '../string-processor'; | ||||
| 
 | ||||
| class AudioProcessor extends StringProcessor { | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(DwengoContentType.AUDIO_MPEG); | ||||
|     } | ||||
|  |  | |||
|  | @ -3,16 +3,16 @@ | |||
|  */ | ||||
| 
 | ||||
| enum DwengoContentType { | ||||
|     TEXT_PLAIN = "text/plain", | ||||
|     TEXT_MARKDOWN = "text/markdown", | ||||
|     IMAGE_BLOCK = "image/image-block", | ||||
|     IMAGE_INLINE = "image/image", | ||||
|     AUDIO_MPEG = "audio/mpeg", | ||||
|     APPLICATION_PDF = "application/pdf", | ||||
|     EXTERN = "extern", | ||||
|     BLOCKLY = "blockly", | ||||
|     GIFT = "text/gift", | ||||
|     CT_SCHEMA = "text/ct-schema" | ||||
|     TEXT_PLAIN = 'text/plain', | ||||
|     TEXT_MARKDOWN = 'text/markdown', | ||||
|     IMAGE_BLOCK = 'image/image-block', | ||||
|     IMAGE_INLINE = 'image/image', | ||||
|     AUDIO_MPEG = 'audio/mpeg', | ||||
|     APPLICATION_PDF = 'application/pdf', | ||||
|     EXTERN = 'extern', | ||||
|     BLOCKLY = 'blockly', | ||||
|     GIFT = 'text/gift', | ||||
|     CT_SCHEMA = 'text/ct-schema', | ||||
| } | ||||
| 
 | ||||
| export { DwengoContentType } | ||||
| export { DwengoContentType }; | ||||
|  |  | |||
|  | @ -5,10 +5,10 @@ | |||
|  */ | ||||
| 
 | ||||
| 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"; | ||||
| import { ProcessingError } from '../processing-error'; | ||||
| import { isValidHttpUrl } from '../../../../util/links'; | ||||
| import { DwengoContentType } from '../content-type'; | ||||
| import { StringProcessor } from '../string-processor'; | ||||
| 
 | ||||
| class ExternProcessor extends StringProcessor { | ||||
|     constructor() { | ||||
|  | @ -17,23 +17,23 @@ class ExternProcessor extends StringProcessor { | |||
| 
 | ||||
|     override renderFn(externURL: string) { | ||||
|         if (!isValidHttpUrl(externURL)) { | ||||
|             throw new ProcessingError("The url is not valid: " + 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) | ||||
|         const match = /(.*youtube.com\/)watch\?v=(.*)/.exec(externURL); | ||||
|         if (match) { | ||||
|             externURL = match[1] + "embed/" + match[2]; | ||||
|             externURL = match[1] + 'embed/' + match[2]; | ||||
|         } | ||||
| 
 | ||||
|         return DOMPurify.sanitize(` | ||||
|         return DOMPurify.sanitize( | ||||
|             ` | ||||
|             <div class="iframe-container"> | ||||
|                 <iframe src="${externURL}" allowfullscreen></iframe> | ||||
|             </div>`,
 | ||||
|             { ADD_TAGS: ["iframe"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling']} | ||||
|             { ADD_TAGS: ['iframe'], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling'] } | ||||
|         ); | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,21 +3,20 @@ | |||
|  */ | ||||
| 
 | ||||
| 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"; | ||||
| import {StringProcessor} from "../string-processor"; | ||||
| 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'; | ||||
| import { StringProcessor } from '../string-processor'; | ||||
| 
 | ||||
| class GiftProcessor extends StringProcessor { | ||||
| 
 | ||||
|     private renderers: RendererMap = { | ||||
|         Category: new CategoryQuestionRenderer(), | ||||
|         Description: new DescriptionQuestionRenderer(), | ||||
|  | @ -26,8 +25,8 @@ class GiftProcessor extends StringProcessor { | |||
|         Numerical: new NumericalQuestionRenderer(), | ||||
|         Short: new ShortQuestionRenderer(), | ||||
|         TF: new TrueFalseQuestionRenderer(), | ||||
|         MC: new MultipleChoiceQuestionRenderer() | ||||
|     } | ||||
|         MC: new MultipleChoiceQuestionRenderer(), | ||||
|     }; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(DwengoContentType.GIFT); | ||||
|  | @ -38,13 +37,13 @@ class GiftProcessor extends StringProcessor { | |||
| 
 | ||||
|         let html = "<div class='learning-object-gift'>\n"; | ||||
|         let i = 1; | ||||
|         for (let question of quizQuestions) { | ||||
|         for (const question of quizQuestions) { | ||||
|             html += `    <div class='gift-question' id='gift-q${i}'>\n`; | ||||
|             html += "        " + this.renderQuestion(question, i).replaceAll(/\n(.+)/g, "\n        $1"); // replace for indentation.
 | ||||
|             html += '        ' + this.renderQuestion(question, i).replaceAll(/\n(.+)/g, '\n        $1'); // Replace for indentation.
 | ||||
|             html += `    </div>\n`; | ||||
|             i++; | ||||
|         } | ||||
|         html += "</div>\n" | ||||
|         html += '</div>\n'; | ||||
| 
 | ||||
|         return DOMPurify.sanitize(html); | ||||
|     } | ||||
|  | @ -56,7 +55,7 @@ class GiftProcessor extends StringProcessor { | |||
| } | ||||
| 
 | ||||
| type RendererMap = { | ||||
|     [K in GIFTQuestion["type"]]: GIFTQuestionRenderer<Extract<GIFTQuestion, { type: K }>> | ||||
|     [K in GIFTQuestion['type']]: GIFTQuestionRenderer<Extract<GIFTQuestion, { type: K }>>; | ||||
| }; | ||||
| 
 | ||||
| export default GiftProcessor; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {GIFTQuestionRenderer} from "./gift-question-renderer"; | ||||
| import {Category} from "gift-pegjs"; | ||||
| import {ProcessingError} from "../../processing-error"; | ||||
| import { GIFTQuestionRenderer } from './gift-question-renderer'; | ||||
| import { Category } from 'gift-pegjs'; | ||||
| import { ProcessingError } from '../../processing-error'; | ||||
| 
 | ||||
| export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> { | ||||
|     render(question: Category, questionNumber: number): string { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {GIFTQuestionRenderer} from "./gift-question-renderer"; | ||||
| import {Description} from "gift-pegjs"; | ||||
| import {ProcessingError} from "../../processing-error"; | ||||
| import { GIFTQuestionRenderer } from './gift-question-renderer'; | ||||
| import { Description } from 'gift-pegjs'; | ||||
| import { ProcessingError } from '../../processing-error'; | ||||
| 
 | ||||
| export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> { | ||||
|     render(question: Description, questionNumber: number): string { | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import {GIFTQuestionRenderer} from "./gift-question-renderer"; | ||||
| import {Essay} from "gift-pegjs"; | ||||
| import { GIFTQuestionRenderer } from './gift-question-renderer'; | ||||
| import { Essay } from 'gift-pegjs'; | ||||
| 
 | ||||
| export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> { | ||||
|     render(question: Essay, questionNumber: number): string { | ||||
|         let renderedHtml = ""; | ||||
|         let renderedHtml = ''; | ||||
|         if (question.title) { | ||||
|             renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`; | ||||
|         } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {GIFTQuestion} from "gift-pegjs"; | ||||
| import { GIFTQuestion } from 'gift-pegjs'; | ||||
| 
 | ||||
| /** | ||||
|  * Subclasses of this class are renderers which can render a specific type of GIFT questions to HTML. | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {GIFTQuestionRenderer} from "./gift-question-renderer"; | ||||
| import {Matching} from "gift-pegjs"; | ||||
| import {ProcessingError} from "../../processing-error"; | ||||
| import { GIFTQuestionRenderer } from './gift-question-renderer'; | ||||
| import { Matching } from 'gift-pegjs'; | ||||
| import { ProcessingError } from '../../processing-error'; | ||||
| 
 | ||||
| export class MatchingQuestionRenderer extends GIFTQuestionRenderer<Matching> { | ||||
|     render(question: Matching, questionNumber: number): string { | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import {GIFTQuestionRenderer} from "./gift-question-renderer"; | ||||
| import {MultipleChoice} from "gift-pegjs"; | ||||
| import { GIFTQuestionRenderer } from './gift-question-renderer'; | ||||
| import { MultipleChoice } from 'gift-pegjs'; | ||||
| 
 | ||||
| export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<MultipleChoice> { | ||||
|     render(question: MultipleChoice, questionNumber: number): string { | ||||
|         let renderedHtml = ""; | ||||
|         let renderedHtml = ''; | ||||
|         if (question.title) { | ||||
|             renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`; | ||||
|         } | ||||
|  | @ -11,7 +11,7 @@ export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<Multipl | |||
|             renderedHtml += `<p class='gift-stem' id='gift-q${questionNumber}-stem'>${question.stem.text}</p>\n`; | ||||
|         } | ||||
|         let i = 0; | ||||
|         for (let choice of question.choices) { | ||||
|         for (const choice of question.choices) { | ||||
|             renderedHtml += `<div class="gift-choice-div">\n`; | ||||
|             renderedHtml += `    <input type='radio' id='gift-q${questionNumber}-choice-${i}' name='gift-q${questionNumber}-choices' value="${i}"/>\n`; | ||||
|             renderedHtml += `    <label for='gift-q${questionNumber}-choice-${i}'>${choice.text}</label>\n`; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {GIFTQuestionRenderer} from "./gift-question-renderer"; | ||||
| import {Numerical} from "gift-pegjs"; | ||||
| import {ProcessingError} from "../../processing-error"; | ||||
| import { GIFTQuestionRenderer } from './gift-question-renderer'; | ||||
| import { Numerical } from 'gift-pegjs'; | ||||
| import { ProcessingError } from '../../processing-error'; | ||||
| 
 | ||||
| export class NumericalQuestionRenderer extends GIFTQuestionRenderer<Numerical> { | ||||
|     render(question: Numerical, questionNumber: number): string { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {GIFTQuestionRenderer} from "./gift-question-renderer"; | ||||
| import {ShortAnswer} from "gift-pegjs"; | ||||
| import {ProcessingError} from "../../processing-error"; | ||||
| import { GIFTQuestionRenderer } from './gift-question-renderer'; | ||||
| import { ShortAnswer } from 'gift-pegjs'; | ||||
| import { ProcessingError } from '../../processing-error'; | ||||
| 
 | ||||
| export class ShortQuestionRenderer extends GIFTQuestionRenderer<ShortAnswer> { | ||||
|     render(question: ShortAnswer, questionNumber: number): string { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {GIFTQuestionRenderer} from "./gift-question-renderer"; | ||||
| import {TrueFalse} from "gift-pegjs"; | ||||
| import {ProcessingError} from "../../processing-error"; | ||||
| import { GIFTQuestionRenderer } from './gift-question-renderer'; | ||||
| import { TrueFalse } from 'gift-pegjs'; | ||||
| import { ProcessingError } from '../../processing-error'; | ||||
| 
 | ||||
| export class TrueFalseQuestionRenderer extends GIFTQuestionRenderer<TrueFalse> { | ||||
|     render(question: TrueFalse, questionNumber: number): string { | ||||
|  |  | |||
|  | @ -2,16 +2,16 @@ | |||
|  * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/image/block_image_processor.js
 | ||||
|  */ | ||||
| 
 | ||||
| import InlineImageProcessor from "./inline-image-processor.js" | ||||
| import InlineImageProcessor from './inline-image-processor.js'; | ||||
| import DOMPurify from 'isomorphic-dompurify'; | ||||
| 
 | ||||
| class BlockImageProcessor extends InlineImageProcessor { | ||||
|     constructor(){ | ||||
|     constructor() { | ||||
|         super(); | ||||
|     } | ||||
| 
 | ||||
|     override renderFn(imageUrl: string){ | ||||
|         let inlineHtml = super.render(imageUrl); | ||||
|     override renderFn(imageUrl: string) { | ||||
|         const inlineHtml = super.render(imageUrl); | ||||
|         return DOMPurify.sanitize(`<div>${inlineHtml}</div>`); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
|  */ | ||||
| 
 | ||||
| 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"; | ||||
| 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 StringProcessor { | ||||
|     constructor(contentType: DwengoContentType = DwengoContentType.IMAGE_INLINE) { | ||||
|  |  | |||
|  | @ -1,15 +1,15 @@ | |||
| /** | ||||
|  * 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 * 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 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 * 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 = marked.Tokens.Image; | ||||
| import Heading = marked.Tokens.Heading; | ||||
|  | @ -27,11 +27,11 @@ const prefixes = { | |||
| }; | ||||
| 
 | ||||
| function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifier { | ||||
|     const [hruid, language, version] = href.split(/\/(.+)/, 2)[1].split("/"); | ||||
|     const [hruid, language, version] = href.split(/\/(.+)/, 2)[1].split('/'); | ||||
|     return { | ||||
|         hruid, | ||||
|         language: language as Language, | ||||
|         version: parseInt(version) | ||||
|         version: parseInt(version), | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
|  | @ -41,69 +41,69 @@ function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifier | |||
|  * - links to other learning objects, | ||||
|  * - embeddings of other learning objects. | ||||
|  */ | ||||
|  const dwengoMarkedRenderer: RendererObject = { | ||||
| const dwengoMarkedRenderer: RendererObject = { | ||||
|     heading(heading: Heading): string { | ||||
|         const text = heading.text; | ||||
|         const level = heading.depth; | ||||
|         const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-'); | ||||
| 
 | ||||
|         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` | ||||
|         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.
 | ||||
|     // Render a custom link when the prefix for a learning object is used.
 | ||||
|     link(link: Link): string { | ||||
|         const href = link.href; | ||||
|         const title = link.title || ""; | ||||
|         const title = link.title || ''; | ||||
|         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
 | ||||
|             // Link to learning-object
 | ||||
|             const learningObjectId = extractLearningObjectIdFromHref(href); | ||||
|             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>`; | ||||
|         } | ||||
|         // 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 => 
 | ||||
|     // render a learning object, pdf, audio or video if a prefix is used.
 | ||||
|     // Render a learning object, pdf, audio or video if a prefix is used.
 | ||||
|     image(img: Image): string { | ||||
|         const href = img.href; | ||||
|         if (href.startsWith(prefixes.learningObject)) { | ||||
|             // embedded learning-object
 | ||||
|             // Embedded learning-object
 | ||||
|             const learningObjectId = extractLearningObjectIdFromHref(href); | ||||
|             return ` | ||||
|                 <learning-object hruid="${learningObjectId.hruid}" language="${learningObjectId.language}" version="${learningObjectId.version}"/> | ||||
|             `; // Placeholder for the learning object since we cannot fetch its HTML here (this has to be a sync function!)
 | ||||
|         } else if (href.startsWith(prefixes.pdf)) { | ||||
|             // embedded pdf
 | ||||
|             let proc = new PdfProcessor(); | ||||
|             // Embedded pdf
 | ||||
|             const proc = new PdfProcessor(); | ||||
|             return proc.render(href.split(/\/(.+)/, 2)[1]); | ||||
|         } else if (href.startsWith(prefixes.audio)) { | ||||
|             // embedded audio
 | ||||
|             let proc = new AudioProcessor(); | ||||
|             // Embedded audio
 | ||||
|             const proc = new AudioProcessor(); | ||||
|             return proc.render(href.split(/\/(.+)/, 2)[1]); | ||||
|         } else if (href.startsWith(prefixes.extern) || href.startsWith(prefixes.video) || href.startsWith(prefixes.notebook)) { | ||||
|             // embedded youtube video or notebook (or other extern content)
 | ||||
|             let proc = new ExternProcessor(); | ||||
|             // Embedded youtube video or notebook (or other extern content)
 | ||||
|             const proc = new ExternProcessor(); | ||||
|             return proc.render(href.split(/\/(.+)/, 2)[1]); | ||||
|         } else { | ||||
|             // embedded image
 | ||||
|             let proc = new InlineImageProcessor(); | ||||
|             return proc.render(href) | ||||
|         } | ||||
|         // Embedded image
 | ||||
|         const proc = new InlineImageProcessor(); | ||||
|         return proc.render(href); | ||||
|     }, | ||||
| } | ||||
| }; | ||||
| 
 | ||||
| export default dwengoMarkedRenderer; | ||||
|  |  | |||
|  | @ -2,12 +2,12 @@ | |||
|  * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/markdown/markdown_processor.js
 | ||||
|  */ | ||||
| 
 | ||||
| import {marked} from 'marked'; | ||||
| import { marked } from 'marked'; | ||||
| import InlineImageProcessor from '../image/inline-image-processor.js'; | ||||
| import {DwengoContentType} from "../content-type"; | ||||
| import dwengoMarkedRenderer from "./dwengo-marked-renderer"; | ||||
| import {StringProcessor} from "../string-processor"; | ||||
| import {ProcessingError} from "../processing-error"; | ||||
| import { DwengoContentType } from '../content-type'; | ||||
| import dwengoMarkedRenderer from './dwengo-marked-renderer'; | ||||
| import { StringProcessor } from '../string-processor'; | ||||
| import { ProcessingError } from '../processing-error'; | ||||
| 
 | ||||
| class MarkdownProcessor extends StringProcessor { | ||||
|     constructor() { | ||||
|  | @ -15,10 +15,10 @@ class MarkdownProcessor extends StringProcessor { | |||
|     } | ||||
| 
 | ||||
|     override renderFn(mdText: string) { | ||||
|         let html = ""; | ||||
|         let html = ''; | ||||
|         try { | ||||
|             marked.use({renderer: dwengoMarkedRenderer}); | ||||
|             html = marked(mdText, {async: false}); | ||||
|             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); | ||||
|  | @ -27,17 +27,11 @@ class MarkdownProcessor extends StringProcessor { | |||
|     } | ||||
| 
 | ||||
|     replaceLinks(html: string) { | ||||
|         let proc = new InlineImageProcessor(); | ||||
|         html = html.replace(/<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g, ( | ||||
|             match: string, | ||||
|             src: string, | ||||
|             alt: string, | ||||
|             altText: string, | ||||
|             title: string, | ||||
|             titleText: string | ||||
|         ) => { | ||||
|             return proc.render(src); | ||||
|         }); | ||||
|         const proc = new InlineImageProcessor(); | ||||
|         html = html.replace( | ||||
|             /<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g, | ||||
|             (match: string, src: string, alt: string, altText: string, title: string, titleText: string) => proc.render(src) | ||||
|         ); | ||||
|         return html; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -5,10 +5,10 @@ | |||
|  */ | ||||
| 
 | ||||
| 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"; | ||||
| 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 StringProcessor { | ||||
|     constructor() { | ||||
|  | @ -20,9 +20,11 @@ class PdfProcessor extends StringProcessor { | |||
|             throw new ProcessingError(`PDF URL is invalid: ${pdfUrl}`); | ||||
|         } | ||||
| 
 | ||||
|         return DOMPurify.sanitize(` | ||||
|         return DOMPurify.sanitize( | ||||
|             ` | ||||
|             <embed src="${pdfUrl}" type="application/pdf" width="100%" height="800px"/> | ||||
|             `, { ADD_TAGS: ["embed"] }
 | ||||
|             `,
 | ||||
|             { ADD_TAGS: ['embed'] } | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,20 +2,20 @@ | |||
|  * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/processing_proxy.js
 | ||||
|  */ | ||||
| 
 | ||||
| import BlockImageProcessor from "./image/block-image-processor.js"; | ||||
| import InlineImageProcessor from "./image/inline-image-processor.js"; | ||||
| import { MarkdownProcessor } from "./markdown/markdown-processor.js"; | ||||
| import TextProcessor from "./text/text-processor.js"; | ||||
| import AudioProcessor from "./audio/audio-processor.js"; | ||||
| import PdfProcessor from "./pdf/pdf-processor.js"; | ||||
| import ExternProcessor from "./extern/extern-processor.js"; | ||||
| import GiftProcessor from "./gift/gift-processor.js"; | ||||
| import {LearningObject} from "../../../entities/content/learning-object.entity"; | ||||
| import Processor from "./processor"; | ||||
| import {DwengoContentType} from "./content-type"; | ||||
| import {LearningObjectIdentifier} from "../../../interfaces/learning-content"; | ||||
| import {Language} from "../../../entities/content/language"; | ||||
| import {replaceAsync} from "../../../util/async"; | ||||
| import BlockImageProcessor from './image/block-image-processor.js'; | ||||
| import InlineImageProcessor from './image/inline-image-processor.js'; | ||||
| import { MarkdownProcessor } from './markdown/markdown-processor.js'; | ||||
| import TextProcessor from './text/text-processor.js'; | ||||
| import AudioProcessor from './audio/audio-processor.js'; | ||||
| import PdfProcessor from './pdf/pdf-processor.js'; | ||||
| import ExternProcessor from './extern/extern-processor.js'; | ||||
| import GiftProcessor from './gift/gift-processor.js'; | ||||
| import { LearningObject } from '../../../entities/content/learning-object.entity'; | ||||
| import Processor from './processor'; | ||||
| import { DwengoContentType } from './content-type'; | ||||
| import { LearningObjectIdentifier } from '../../../interfaces/learning-content'; | ||||
| import { Language } from '../../../entities/content/language'; | ||||
| import { replaceAsync } from '../../../util/async'; | ||||
| 
 | ||||
| const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" language="([^"]+)" version="([^"]+)"\/>/g; | ||||
| const LEARNING_OBJECT_DOES_NOT_EXIST = "<div class='non-existing-learning-object' />"; | ||||
|  | @ -32,12 +32,10 @@ class ProcessingService { | |||
|             new AudioProcessor(), | ||||
|             new PdfProcessor(), | ||||
|             new ExternProcessor(), | ||||
|             new GiftProcessor() | ||||
|             new GiftProcessor(), | ||||
|         ]; | ||||
| 
 | ||||
|         this.processors = new Map( | ||||
|             processors.map(processor => [processor.contentType, processor]) | ||||
|         ) | ||||
|         this.processors = new Map(processors.map((processor) => [processor.contentType, processor])); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -54,7 +52,7 @@ class ProcessingService { | |||
|         learningObject: LearningObject, | ||||
|         fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifier) => Promise<LearningObject | null> | ||||
|     ): Promise<string> { | ||||
|         let html = this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject); | ||||
|         const html = this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject); | ||||
|         if (fetchEmbeddedLearningObjects) { | ||||
|             // Replace all embedded learning objects.
 | ||||
|             return replaceAsync( | ||||
|  | @ -65,7 +63,7 @@ class ProcessingService { | |||
|                     const learningObject = await fetchEmbeddedLearningObjects({ | ||||
|                         hruid, | ||||
|                         language: language as Language, | ||||
|                         version: parseInt(version) | ||||
|                         version: parseInt(version), | ||||
|                     }); | ||||
| 
 | ||||
|                     // If it does not exist, replace it by a placeholder.
 | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {LearningObject} from "../../../entities/content/learning-object.entity"; | ||||
| import {ProcessingError} from "./processing-error"; | ||||
| import {DwengoContentType} from "./content-type"; | ||||
| import { LearningObject } from '../../../entities/content/learning-object.entity'; | ||||
| import { ProcessingError } from './processing-error'; | ||||
| import { DwengoContentType } from './content-type'; | ||||
| 
 | ||||
| /** | ||||
|  * Abstract base class for all processors. | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import Processor from "./processor"; | ||||
| import {LearningObject} from "../../../entities/content/learning-object.entity"; | ||||
| import Processor from './processor'; | ||||
| import { LearningObject } from '../../../entities/content/learning-object.entity'; | ||||
| 
 | ||||
| export abstract class StringProcessor extends Processor<string> { | ||||
|     /** | ||||
|  | @ -14,6 +14,6 @@ export abstract class StringProcessor extends Processor<string> { | |||
|      * @protected | ||||
|      */ | ||||
|     protected renderLearningObjectFn(toRender: LearningObject): string { | ||||
|         return this.render(toRender.content.toString("ascii")); | ||||
|         return this.render(toRender.content.toString('ascii')); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -3,8 +3,8 @@ | |||
|  */ | ||||
| 
 | ||||
| import DOMPurify from 'isomorphic-dompurify'; | ||||
| import {DwengoContentType} from "../content-type.js"; | ||||
| import {StringProcessor} from "../string-processor"; | ||||
| import { DwengoContentType } from '../content-type.js'; | ||||
| import { StringProcessor } from '../string-processor'; | ||||
| 
 | ||||
| class TextProcessor extends StringProcessor { | ||||
|     constructor() { | ||||
|  |  | |||
|  | @ -1,19 +1,11 @@ | |||
| import {LearningPathProvider} from "./learning-path-provider"; | ||||
| import { | ||||
|     FilteredLearningObject, | ||||
|     LearningObjectNode, | ||||
|     LearningPath, | ||||
|     LearningPathResponse, | ||||
|     Transition | ||||
| } from "../../interfaces/learning-content"; | ||||
| import { | ||||
|     LearningPath as LearningPathEntity | ||||
| } from "../../entities/content/learning-path.entity" | ||||
| import {getLearningPathRepository} from "../../data/repositories"; | ||||
| import {Language} from "../../entities/content/language"; | ||||
| import learningObjectService from "../learning-objects/learning-object-service"; | ||||
| import { LearningPathNode } from "../../entities/content/learning-path-node.entity"; | ||||
| import {LearningPathTransition} from "../../entities/content/learning-path-transition.entity"; | ||||
| import { LearningPathProvider } from './learning-path-provider'; | ||||
| import { FilteredLearningObject, LearningObjectNode, LearningPath, LearningPathResponse, Transition } from '../../interfaces/learning-content'; | ||||
| import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.entity'; | ||||
| import { getLearningPathRepository } from '../../data/repositories'; | ||||
| import { Language } from '../../entities/content/language'; | ||||
| import learningObjectService from '../learning-objects/learning-object-service'; | ||||
| import { LearningPathNode } from '../../entities/content/learning-path-node.entity'; | ||||
| import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity'; | ||||
| 
 | ||||
| /** | ||||
|  * Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its | ||||
|  | @ -22,22 +14,22 @@ import {LearningPathTransition} from "../../entities/content/learning-path-trans | |||
|  */ | ||||
| async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Map<LearningPathNode, FilteredLearningObject>> { | ||||
|     // Fetching the corresponding learning object for each of the nodes and creating a map that maps each node to
 | ||||
|     // its corresponding learning object.
 | ||||
|     // Its corresponding learning object.
 | ||||
|     const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>( | ||||
|         await Promise.all( | ||||
|             nodes.map(node => | ||||
|                 learningObjectService.getLearningObjectById({ | ||||
|                     hruid: node.learningObjectHruid, | ||||
|                     version: node.version, | ||||
|                     language: node.language | ||||
|                 }).then(learningObject => | ||||
|                     <[LearningPathNode, FilteredLearningObject | null]>[node, learningObject] | ||||
|                 ) | ||||
|             nodes.map((node) => | ||||
|                 learningObjectService | ||||
|                     .getLearningObjectById({ | ||||
|                         hruid: node.learningObjectHruid, | ||||
|                         version: node.version, | ||||
|                         language: node.language, | ||||
|                     }) | ||||
|                     .then((learningObject) => <[LearningPathNode, FilteredLearningObject | null]>[node, learningObject]) | ||||
|             ) | ||||
|         ) | ||||
|     ); | ||||
|     if (nullableNodesToLearningObjects.values().some(it => it === null)) { | ||||
|         throw new Error("At least one of the learning objects on this path could not be found.") | ||||
|     if (nullableNodesToLearningObjects.values().some((it) => it === null)) { | ||||
|         throw new Error('At least one of the learning objects on this path could not be found.'); | ||||
|     } | ||||
|     return nullableNodesToLearningObjects as Map<LearningPathNode, FilteredLearningObject>; | ||||
| } | ||||
|  | @ -46,19 +38,22 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma | |||
|  * Convert the given learning path entity to an object which conforms to the learning path content. | ||||
|  */ | ||||
| async function convertLearningPath(learningPath: LearningPathEntity, order: number): Promise<LearningPath> { | ||||
|     const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = | ||||
|         await getLearningObjectsForNodes(learningPath.nodes); | ||||
|     const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes); | ||||
| 
 | ||||
|     const targetAges = | ||||
|         nodesToLearningObjects.values().flatMap(it => it.targetAges || []).toArray(); | ||||
|     const targetAges = nodesToLearningObjects | ||||
|         .values() | ||||
|         .flatMap((it) => it.targetAges || []) | ||||
|         .toArray(); | ||||
| 
 | ||||
|     const keywords = | ||||
|         nodesToLearningObjects.values().flatMap(it => it.keywords || []).toArray(); | ||||
|     const keywords = nodesToLearningObjects | ||||
|         .values() | ||||
|         .flatMap((it) => it.keywords || []) | ||||
|         .toArray(); | ||||
| 
 | ||||
|     const image = learningPath.image ? learningPath.image.toString("base64") : undefined; | ||||
|     const image = learningPath.image ? learningPath.image.toString('base64') : undefined; | ||||
| 
 | ||||
|     return { | ||||
|         _id: `${learningPath.hruid}/${learningPath.language}`, // for backwards compatibility with the original Dwengo API.
 | ||||
|         _id: `${learningPath.hruid}/${learningPath.language}`, // For backwards compatibility with the original Dwengo API.
 | ||||
|         __order: order, | ||||
|         hruid: learningPath.hruid, | ||||
|         language: learningPath.language, | ||||
|  | @ -71,8 +66,8 @@ async function convertLearningPath(learningPath: LearningPathEntity, order: numb | |||
|         keywords: keywords.join(' '), | ||||
|         target_ages: targetAges, | ||||
|         max_age: Math.max(...targetAges), | ||||
|         min_age: Math.min(...targetAges) | ||||
|     } | ||||
|         min_age: Math.min(...targetAges), | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -80,24 +75,23 @@ async function convertLearningPath(learningPath: LearningPathEntity, order: numb | |||
|  * learning objects into a list of learning path nodes as they should be represented in the API. | ||||
|  * @param nodesToLearningObjects | ||||
|  */ | ||||
| function convertNodes( | ||||
|     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> | ||||
| ): LearningObjectNode[]  { | ||||
|     return nodesToLearningObjects.entries().map((entry) => { | ||||
|         const [node, learningObject] = entry; | ||||
|         return { | ||||
|             _id: learningObject.uuid, | ||||
|             language: learningObject.language, | ||||
|             start_node: node.startNode, | ||||
|             created_at: node.createdAt.toISOString(), | ||||
|             updatedAt: node.updatedAt.toISOString(), | ||||
|             learningobject_hruid: node.learningObjectHruid, | ||||
|             version: learningObject.version, | ||||
|             transitions: node.transitions.map((trans, i) => | ||||
|                 convertTransition(trans, i, nodesToLearningObjects) | ||||
|             ) | ||||
|         } | ||||
|     }).toArray(); | ||||
| function convertNodes(nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>): LearningObjectNode[] { | ||||
|     return nodesToLearningObjects | ||||
|         .entries() | ||||
|         .map((entry) => { | ||||
|             const [node, learningObject] = entry; | ||||
|             return { | ||||
|                 _id: learningObject.uuid, | ||||
|                 language: learningObject.language, | ||||
|                 start_node: node.startNode, | ||||
|                 created_at: node.createdAt.toISOString(), | ||||
|                 updatedAt: node.updatedAt.toISOString(), | ||||
|                 learningobject_hruid: node.learningObjectHruid, | ||||
|                 version: learningObject.version, | ||||
|                 transitions: node.transitions.map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), | ||||
|             }; | ||||
|         }) | ||||
|         .toArray(); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -115,17 +109,17 @@ function convertTransition( | |||
| ): Transition { | ||||
|     const nextNode = nodesToLearningObjects.get(transition.next); | ||||
|     if (!nextNode) { | ||||
|         throw new Error(`Learning object ${transition.next.learningObjectHruid}/${transition.next.language}/${transition.next.version} not found!`) | ||||
|         throw new Error(`Learning object ${transition.next.learningObjectHruid}/${transition.next.language}/${transition.next.version} not found!`); | ||||
|     } else { | ||||
|         return { | ||||
|             _id: "" + index, // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
 | ||||
|             _id: '' + index, // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
 | ||||
|             default: false, // We don't work with default transitions but retain this for backwards compatibility.
 | ||||
|             next: { | ||||
|                 _id: nextNode._id + index, // Construct a unique ID for the transition for backwards compatibility.
 | ||||
|                 hruid: transition.next.learningObjectHruid, | ||||
|                 language: nextNode.language, | ||||
|                 version: nextNode.version | ||||
|             } | ||||
|                 version: nextNode.version, | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | @ -140,19 +134,15 @@ const databaseLearningPathProvider: LearningPathProvider = { | |||
|     async fetchLearningPaths(hruids: string[], language: Language, source: string): Promise<LearningPathResponse> { | ||||
|         const learningPathRepo = getLearningPathRepository(); | ||||
| 
 | ||||
|         const learningPaths = await Promise.all( | ||||
|             hruids.map(hruid => learningPathRepo.findByHruidAndLanguage(hruid, language)) | ||||
|         ); | ||||
|         const learningPaths = await Promise.all(hruids.map((hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language))); | ||||
|         const filteredLearningPaths = await Promise.all( | ||||
|             learningPaths | ||||
|                 .filter(learningPath => learningPath !== null) | ||||
|                 .map((learningPath, index) => convertLearningPath(learningPath, index)) | ||||
|             learningPaths.filter((learningPath) => learningPath !== null).map((learningPath, index) => convertLearningPath(learningPath, index)) | ||||
|         ); | ||||
| 
 | ||||
|         return { | ||||
|             success: filteredLearningPaths.length > 0, | ||||
|             data: await Promise.all(filteredLearningPaths), | ||||
|             source | ||||
|             source, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|  | @ -163,12 +153,8 @@ const databaseLearningPathProvider: LearningPathProvider = { | |||
|         const learningPathRepo = getLearningPathRepository(); | ||||
| 
 | ||||
|         const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language); | ||||
|         return await Promise.all( | ||||
|             searchResults.map((result, index) => | ||||
|                 convertLearningPath(result, index) | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|         return await Promise.all(searchResults.map((result, index) => convertLearningPath(result, index))); | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export default databaseLearningPathProvider; | ||||
|  |  | |||
|  | @ -1,17 +1,10 @@ | |||
| import { fetchWithLogging } from '../../util/apiHelper.js'; | ||||
| import { DWENGO_API_BASE } from '../../config.js'; | ||||
| import { | ||||
|     LearningPath, | ||||
|     LearningPathResponse, | ||||
| } from '../../interfaces/learning-content.js'; | ||||
| import {LearningPathProvider} from "./learning-path-provider"; | ||||
| import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js'; | ||||
| import { LearningPathProvider } from './learning-path-provider'; | ||||
| 
 | ||||
| const dwengoApiLearningPathProvider: LearningPathProvider = { | ||||
|     async fetchLearningPaths( | ||||
|         hruids: string[], | ||||
|         language: string, | ||||
|         source: string | ||||
|     ): Promise<LearningPathResponse> { | ||||
|     async fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse> { | ||||
|         if (hruids.length === 0) { | ||||
|             return { | ||||
|                 success: false, | ||||
|  | @ -24,11 +17,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | |||
|         const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; | ||||
|         const params = { pathIdList: JSON.stringify({ hruids }), language }; | ||||
| 
 | ||||
|         const learningPaths = await fetchWithLogging<LearningPath[]>( | ||||
|             apiUrl, | ||||
|             `Learning paths for ${source}`, | ||||
|             { params } | ||||
|         ); | ||||
|         const learningPaths = await fetchWithLogging<LearningPath[]>(apiUrl, `Learning paths for ${source}`, { params }); | ||||
| 
 | ||||
|         if (!learningPaths || learningPaths.length === 0) { | ||||
|             console.error(`⚠️ WARNING: No learning paths found for ${source}.`); | ||||
|  | @ -46,20 +35,13 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | |||
|             data: learningPaths, | ||||
|         }; | ||||
|     }, | ||||
|     async searchLearningPaths( | ||||
|         query: string, | ||||
|         language: string | ||||
|     ): Promise<LearningPath[]> { | ||||
|     async searchLearningPaths(query: string, language: string): Promise<LearningPath[]> { | ||||
|         const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; | ||||
|         const params = { all: query, language }; | ||||
| 
 | ||||
|         const searchResults = await fetchWithLogging<LearningPath[]>( | ||||
|             apiUrl, | ||||
|             `Search learning paths with query "${query}"`, | ||||
|             { params } | ||||
|         ); | ||||
|         const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params }); | ||||
|         return searchResults ?? []; | ||||
|     } | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export default dwengoApiLearningPathProvider; | ||||
|  |  | |||
|  | @ -1,21 +1,21 @@ | |||
| import {Student} from "../../entities/users/student.entity"; | ||||
| import {getSubmissionRepository} from "../../data/repositories"; | ||||
| import {Group} from "../../entities/assignments/group.entity"; | ||||
| import {Submission} from "../../entities/assignments/submission.entity"; | ||||
| import {LearningObjectIdentifier} from "../../entities/content/learning-object-identifier"; | ||||
| import {LearningPathNode} from "../../entities/content/learning-path-node.entity"; | ||||
| import {LearningPathTransition} from "../../entities/content/learning-path-transition.entity"; | ||||
| import {JSONPath} from 'jsonpath-plus'; | ||||
| import { Student } from '../../entities/users/student.entity'; | ||||
| import { getSubmissionRepository } from '../../data/repositories'; | ||||
| import { Group } from '../../entities/assignments/group.entity'; | ||||
| import { Submission } from '../../entities/assignments/submission.entity'; | ||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier'; | ||||
| import { LearningPathNode } from '../../entities/content/learning-path-node.entity'; | ||||
| import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity'; | ||||
| import { JSONPath } from 'jsonpath-plus'; | ||||
| 
 | ||||
| /** | ||||
|  * Returns the last submission for the learning object associated with the given node and for the student or group | ||||
|  */ | ||||
| async function getLastRelevantSubmission(node: LearningPathNode, pathFor: {student?: Student, group?: Group}): Promise<Submission | null> { | ||||
| async function getLastRelevantSubmission(node: LearningPathNode, pathFor: { student?: Student; group?: Group }): Promise<Submission | null> { | ||||
|     const submissionRepo = getSubmissionRepository(); | ||||
|     const learningObjectId: LearningObjectIdentifier = { | ||||
|         hruid: node.learningObjectHruid, | ||||
|         language: node.language, | ||||
|         version: node.version | ||||
|         version: node.version, | ||||
|     }; | ||||
|     let lastSubmission: Submission | null; | ||||
|     if (pathFor.group) { | ||||
|  | @ -23,47 +23,46 @@ async function getLastRelevantSubmission(node: LearningPathNode, pathFor: {stude | |||
|     } else if (pathFor.student) { | ||||
|         lastSubmission = await submissionRepo.findMostRecentSubmissionForStudent(learningObjectId, pathFor.student); | ||||
|     } else { | ||||
|         throw new Error("The path must either be created for a certain group or for a certain student!"); | ||||
|         throw new Error('The path must either be created for a certain group or for a certain student!'); | ||||
|     } | ||||
|     return lastSubmission; | ||||
| } | ||||
| 
 | ||||
| function transitionPossible(transition: LearningPathTransition, submitted: object | null): boolean { | ||||
|     if (transition.condition === "true" || !transition.condition) { | ||||
|     if (transition.condition === 'true' || !transition.condition) { | ||||
|         return true; // If the transition is unconditional, we can go on.
 | ||||
|     } | ||||
|     if (submitted === null) { | ||||
|         return false; // If the transition is not unconditional and there was no submission, the transition is not possible.
 | ||||
|     } | ||||
|     return JSONPath({path: transition.condition, json: submitted}).length === 0; | ||||
|     return JSONPath({ path: transition.condition, json: submitted }).length === 0; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Service to create individual trajectories from learning paths based on the submissions of the student or group. | ||||
|  */ | ||||
| const learningPathPersonalizingService = { | ||||
|     async calculatePersonalizedTrajectory(nodes: LearningPathNode[], pathFor: {student?: Student, group?: Group}): Promise<LearningPathNode[]> { | ||||
|         let trajectory: LearningPathNode[] = []; | ||||
|     async calculatePersonalizedTrajectory(nodes: LearningPathNode[], pathFor: { student?: Student; group?: Group }): Promise<LearningPathNode[]> { | ||||
|         const trajectory: LearningPathNode[] = []; | ||||
| 
 | ||||
|         // Always start with the start node.
 | ||||
|         let currentNode = nodes.filter(it => it.startNode)[0]; | ||||
|         let currentNode = nodes.filter((it) => it.startNode)[0]; | ||||
|         trajectory.push(currentNode); | ||||
| 
 | ||||
|         while (true) { | ||||
|             // At every node, calculate all the possible next transitions.
 | ||||
|             let lastSubmission = await getLastRelevantSubmission(currentNode, pathFor); | ||||
|             let submitted = lastSubmission === null ? null : JSON.parse(lastSubmission.content); | ||||
|             let possibleTransitions = currentNode.transitions | ||||
|                 .filter(it => transitionPossible(it, submitted)); | ||||
|             const lastSubmission = await getLastRelevantSubmission(currentNode, pathFor); | ||||
|             const submitted = lastSubmission === null ? null : JSON.parse(lastSubmission.content); | ||||
|             const possibleTransitions = currentNode.transitions.filter((it) => transitionPossible(it, submitted)); | ||||
| 
 | ||||
|             if (possibleTransitions.length === 0) { // If there are none, the trajectory has ended.
 | ||||
|             if (possibleTransitions.length === 0) { | ||||
|                 // If there are none, the trajectory has ended.
 | ||||
|                 return trajectory; | ||||
|             } else { // Otherwise, take the first possible transition.
 | ||||
|                 currentNode = possibleTransitions[0].node; | ||||
|                 trajectory.push(currentNode); | ||||
|             } | ||||
|             } // Otherwise, take the first possible transition.
 | ||||
|             currentNode = possibleTransitions[0].node; | ||||
|             trajectory.push(currentNode); | ||||
|         } | ||||
|     } | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export default learningPathPersonalizingService; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; | ||||
| import {Language} from "../../entities/content/language"; | ||||
| import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content'; | ||||
| import { Language } from '../../entities/content/language'; | ||||
| 
 | ||||
| /** | ||||
|  * Generic interface for a service which provides access to learning paths from a data source. | ||||
|  |  | |||
|  | @ -1,14 +1,11 @@ | |||
| import { | ||||
|     LearningPath, | ||||
|     LearningPathResponse | ||||
| } from "../../interfaces/learning-content"; | ||||
| import dwengoApiLearningPathProvider from "./dwengo-api-learning-path-provider"; | ||||
| import databaseLearningPathProvider from "./database-learning-path-provider"; | ||||
| import {EnvVars, getEnvVar} from "../../util/envvars"; | ||||
| import {Language} from "../../entities/content/language"; | ||||
| import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content'; | ||||
| import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider'; | ||||
| import databaseLearningPathProvider from './database-learning-path-provider'; | ||||
| import { EnvVars, getEnvVar } from '../../util/envvars'; | ||||
| import { Language } from '../../entities/content/language'; | ||||
| 
 | ||||
| const userContentPrefix = getEnvVar(EnvVars.UserContentPrefix); | ||||
| const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider] | ||||
| const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider]; | ||||
| 
 | ||||
| /** | ||||
|  * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) | ||||
|  | @ -18,18 +15,16 @@ const learningPathService = { | |||
|      * Fetch the learning paths with the given hruids from the data source. | ||||
|      */ | ||||
|     async fetchLearningPaths(hruids: string[], language: Language, source: string): Promise<LearningPathResponse> { | ||||
|         const userContentHruids = hruids.filter(hruid => hruid.startsWith(userContentPrefix)); | ||||
|         const nonUserContentHruids = hruids.filter(hruid => !hruid.startsWith(userContentPrefix)); | ||||
|         const userContentHruids = hruids.filter((hruid) => hruid.startsWith(userContentPrefix)); | ||||
|         const nonUserContentHruids = hruids.filter((hruid) => !hruid.startsWith(userContentPrefix)); | ||||
| 
 | ||||
|         const userContentLearningPaths = | ||||
|             await databaseLearningPathProvider.fetchLearningPaths(userContentHruids, language, source); | ||||
|         const nonUserContentLearningPaths | ||||
|             = await dwengoApiLearningPathProvider.fetchLearningPaths(nonUserContentHruids, language, source); | ||||
|         const userContentLearningPaths = await databaseLearningPathProvider.fetchLearningPaths(userContentHruids, language, source); | ||||
|         const nonUserContentLearningPaths = await dwengoApiLearningPathProvider.fetchLearningPaths(nonUserContentHruids, language, source); | ||||
| 
 | ||||
|         return { | ||||
|             data: (userContentLearningPaths.data || []).concat(nonUserContentLearningPaths.data || []), | ||||
|             source: source, | ||||
|             success: userContentLearningPaths.success || nonUserContentLearningPaths.success | ||||
|             success: userContentLearningPaths.success || nonUserContentLearningPaths.success, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|  | @ -37,13 +32,9 @@ const learningPathService = { | |||
|      * Search learning paths in the data source using the given search string. | ||||
|      */ | ||||
|     async searchLearningPaths(query: string, language: Language): Promise<LearningPath[]> { | ||||
|         const providerResponses = await Promise.all( | ||||
|             allProviders.map( | ||||
|                 provider => provider.searchLearningPaths(query, language) | ||||
|             ) | ||||
|         ); | ||||
|         const providerResponses = await Promise.all(allProviders.map((provider) => provider.searchLearningPaths(query, language))); | ||||
|         return providerResponses.flat(); | ||||
|     } | ||||
| } | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export default learningPathService; | ||||
|  |  | |||
|  | @ -17,9 +17,9 @@ export async function fetchWithLogging<T>( | |||
|     url: string, | ||||
|     description: string, | ||||
|     options?: { | ||||
|         params?: Record<string, any>, | ||||
|         query?: Record<string, any>, | ||||
|         responseType?: "json" | "text", | ||||
|         params?: Record<string, any>; | ||||
|         query?: Record<string, any>; | ||||
|         responseType?: 'json' | 'text'; | ||||
|     } | ||||
| ): Promise<T | null> { | ||||
|     try { | ||||
|  |  | |||
|  | @ -16,8 +16,8 @@ export async function replaceAsync(str: string, regex: RegExp, replacementFn: (m | |||
|     }); | ||||
| 
 | ||||
|     // Wait for the replacements to get loaded. Reverse them so when popping them, we work in a FIFO manner.
 | ||||
|     const replacements: string[] = (await Promise.all(promises)); | ||||
|     const replacements: string[] = await Promise.all(promises); | ||||
| 
 | ||||
|     // Second run through matches: Replace them by their previously computed replacements.
 | ||||
|     return str.replace(regex, () => replacements.pop()!!); | ||||
|     return str.replace(regex, () => replacements.pop()!); | ||||
| } | ||||
|  |  | |||
|  | @ -15,9 +15,9 @@ export const EnvVars: { [key: string]: EnvVar } = { | |||
|     DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, | ||||
|     DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, | ||||
|     DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, | ||||
|     LearningContentRepoApiBaseUrl: { key: PREFIX + "LEARNING_CONTENT_REPO_API_BASE_URL", defaultValue: "https://dwengo.org/backend/api"}, | ||||
|     FallbackLanguage: { key: PREFIX + "FALLBACK_LANGUAGE", defaultValue: "nl" }, | ||||
|     UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: "u_" }, | ||||
|     LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' }, | ||||
|     FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' }, | ||||
|     UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: 'u_' }, | ||||
|     IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true }, | ||||
|     IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', required: true }, | ||||
|     IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import {LearningObjectIdentifier} from "../interfaces/learning-content"; | ||||
| import { LearningObjectIdentifier } from '../interfaces/learning-content'; | ||||
| 
 | ||||
| export function isValidHttpUrl(url: string): boolean { | ||||
|     try { | ||||
|         const parsedUrl = new URL(url, "http://test.be"); | ||||
|         return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"; | ||||
|         const parsedUrl = new URL(url, 'http://test.be'); | ||||
|         return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'; | ||||
|     } catch (e) { | ||||
|         return false; | ||||
|     } | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger