refactor(backend): Streamlining van de testdata voor leerpaden en leerobjecten + integratie in seed
Hierbij ook testdata functionaliteit toegevoegd om makkelijk nieuwe leerpaden aan te maken.
This commit is contained in:
		
							parent
							
								
									4092f1f617
								
							
						
					
					
						commit
						202cf4e33c
					
				
					 32 changed files with 691 additions and 493 deletions
				
			
		|  | @ -1,6 +1,10 @@ | |||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||
| import { LearningPath } from '../../entities/content/learning-path.entity.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import {LearningPathNode} from "../../entities/content/learning-path-node.entity"; | ||||
| import {RequiredEntityData} from "@mikro-orm/core"; | ||||
| import {LearningPathTransition} from "../../entities/content/learning-path-transition.entity"; | ||||
| import {EntityAlreadyExistsException} from "../../exceptions/entity-already-exists-exception"; | ||||
| 
 | ||||
| export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | ||||
|     public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { | ||||
|  | @ -23,4 +27,33 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath> | |||
|             populate: ['nodes', 'nodes.transitions'], | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public createNode( | ||||
|         nodeData: RequiredEntityData<LearningPathNode, never, false> | ||||
|     ): LearningPathNode { | ||||
|         return this.em.create(LearningPathNode, nodeData); | ||||
|     } | ||||
| 
 | ||||
|     public createTransition( | ||||
|         transitionData: RequiredEntityData<LearningPathTransition, never, false> | ||||
|     ): LearningPathTransition { | ||||
|         return this.em.create(LearningPathTransition, transitionData) | ||||
|     } | ||||
| 
 | ||||
|     public async saveLearningPathNodesAndTransitions( | ||||
|         path: LearningPath, | ||||
|         nodes: LearningPathNode[], | ||||
|         transitions: LearningPathTransition[], | ||||
|         options?: {preventOverwrite?: boolean} | ||||
|     ): Promise<void> { | ||||
|         if (options?.preventOverwrite && (await this.findOne(path))) { | ||||
|             throw new EntityAlreadyExistsException( | ||||
|                 "A learning path with this hruid/language combination already exists." | ||||
|             ); | ||||
|         } | ||||
|         const em = this.getEntityManager(); | ||||
|         await em.persistAndFlush(path); | ||||
|         await Promise.all(nodes.map(it => em.persistAndFlush(it))); | ||||
|         await Promise.all(transitions.map(it => em.persistAndFlush(it))); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; | ||||
| import {Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel} from '@mikro-orm/core'; | ||||
| import { LearningPath } from './learning-path.entity.js'; | ||||
| import { LearningPathTransition } from './learning-path-transition.entity.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
|  | @ -27,7 +27,7 @@ export class LearningPathNode { | |||
|     startNode!: boolean; | ||||
| 
 | ||||
|     @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' }) | ||||
|     transitions: LearningPathTransition[] = []; | ||||
|     transitions: Collection<LearningPathTransition> = new Collection<LearningPathTransition>(this); | ||||
| 
 | ||||
|     @Property({ length: 3 }) | ||||
|     createdAt: Date = new Date(); | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import {Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property} from '@mikro-orm/core'; | ||||
| import { Teacher } from '../users/teacher.entity.js'; | ||||
| import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; | ||||
| import { LearningPathNode } from './learning-path-node.entity.js'; | ||||
|  | @ -25,5 +25,5 @@ export class LearningPath { | |||
|     image: Buffer | null = null; | ||||
| 
 | ||||
|     @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' }) | ||||
|     nodes: LearningPathNode[] = []; | ||||
|     nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this); | ||||
| } | ||||
|  |  | |||
|  | @ -14,13 +14,14 @@ import { | |||
| } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import {Group} from "../../entities/assignments/group.entity"; | ||||
| import {Collection} from "@mikro-orm/core"; | ||||
| 
 | ||||
| /** | ||||
|  * Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its | ||||
|  * corresponding learning object. | ||||
|  * @param nodes The nodes to find the learning object for. | ||||
|  */ | ||||
| async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Map<LearningPathNode, FilteredLearningObject>> { | ||||
| async function getLearningObjectsForNodes(nodes: Collection<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.
 | ||||
|     const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>( | ||||
|  | @ -208,7 +209,7 @@ const databaseLearningPathProvider: LearningPathProvider = { | |||
| 
 | ||||
|         const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language); | ||||
|         return await Promise.all(searchResults.map(async (result, index) => convertLearningPath(result, index, personalizedFor))); | ||||
|     }, | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export default databaseLearningPathProvider; | ||||
|  |  | |||
|  | @ -1,13 +1,79 @@ | |||
| import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; | ||||
| import databaseLearningPathProvider from './database-learning-path-provider.js'; | ||||
| import { envVars, getEnvVar } from '../../util/envVars.js'; | ||||
| import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| import {LearningObjectNode, LearningPath, LearningPathResponse} from '@dwengo-1/common/interfaces/learning-content'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import {Group} from "../../entities/assignments/group.entity"; | ||||
| import {LearningPath as LearningPathEntity} from "../../entities/content/learning-path.entity"; | ||||
| import {getLearningPathRepository} from "../../data/repositories"; | ||||
| import {LearningPathNode} from "../../entities/content/learning-path-node.entity"; | ||||
| import {LearningPathTransition} from "../../entities/content/learning-path-transition.entity"; | ||||
| import {base64ToArrayBuffer} from "../../util/base64-buffer-conversion"; | ||||
| import {TeacherDTO} from "@dwengo-1/common/interfaces/teacher"; | ||||
| import {mapToTeacher} from "../../interfaces/teacher"; | ||||
| import {Collection} from "@mikro-orm/core"; | ||||
| 
 | ||||
| const userContentPrefix = getEnvVar(envVars.UserContentPrefix); | ||||
| const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider]; | ||||
| 
 | ||||
| export function mapToLearningPath( | ||||
|     dto: LearningPath, adminsDto: TeacherDTO[] | ||||
| ): LearningPathEntity { | ||||
|     const admins = adminsDto.map(admin => mapToTeacher(admin)); | ||||
|     const repo = getLearningPathRepository(); | ||||
|     const path = repo.create({ | ||||
|         hruid: dto.hruid, | ||||
|         language: dto.language as Language, | ||||
|         description: dto.description, | ||||
|         title: dto.title, | ||||
|         admins, | ||||
|         image: dto.image ? Buffer.from(base64ToArrayBuffer(dto.image)) : null | ||||
|     }); | ||||
|     const nodes = dto.nodes.map((nodeDto: LearningObjectNode, i: number) => | ||||
|         repo.createNode({ | ||||
|             learningPath: path, | ||||
|             learningObjectHruid: nodeDto.learningobject_hruid, | ||||
|             language: nodeDto.language, | ||||
|             version: nodeDto.version, | ||||
|             startNode: nodeDto.start_node ?? false, | ||||
|             nodeNumber: i, | ||||
|             createdAt: new Date(), | ||||
|             updatedAt: new Date() | ||||
|         }) | ||||
|     ); | ||||
|     dto.nodes.forEach(nodeDto => { | ||||
|         const fromNode = nodes.find(it => | ||||
|             it.learningObjectHruid === nodeDto.learningobject_hruid | ||||
|             && it.language === nodeDto.language | ||||
|             && it.version === nodeDto.version | ||||
|         )!; | ||||
|         const transitions = nodeDto.transitions.map((transDto, i) => { | ||||
|             const toNode = nodes.find(it => | ||||
|                 it.learningObjectHruid === transDto.next.hruid | ||||
|                 && it.language === transDto.next.language | ||||
|                 && it.version === transDto.next.version | ||||
|             ); | ||||
| 
 | ||||
|             if (toNode) { | ||||
|                 return repo.createTransition({ | ||||
|                     transitionNumber: i, | ||||
|                     node: fromNode, | ||||
|                     next: toNode, | ||||
|                     condition: transDto.condition ?? "true" | ||||
|                 }); | ||||
|             } else { | ||||
|                 return undefined; | ||||
|             } | ||||
|         }).filter(it => it).map(it => it!); | ||||
| 
 | ||||
|         fromNode.transitions = new Collection<LearningPathTransition>(transitions); | ||||
|     }); | ||||
| 
 | ||||
|     path.nodes = new Collection<LearningPathNode>(nodes); | ||||
| 
 | ||||
|     return path; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) | ||||
|  */ | ||||
|  | @ -54,6 +120,17 @@ const learningPathService = { | |||
|         ); | ||||
|         return providerResponses.flat(); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Add a new learning path to the database. | ||||
|      * @param dto Learning path DTO from which the learning path will be created. | ||||
|      * @param admins Teachers who should become an admin of the learning path. | ||||
|      */ | ||||
|     async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<void> { | ||||
|         const repo = getLearningPathRepository(); | ||||
|         const path = mapToLearningPath(dto, admins); | ||||
|         await repo.save(path, {preventOverwrite: true}) | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export default learningPathService; | ||||
|  |  | |||
							
								
								
									
										12
									
								
								backend/src/util/base64-buffer-conversion.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/util/base64-buffer-conversion.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| /** | ||||
|  * Convert a Base64-encoded string into a buffer with the same data. | ||||
|  * @param base64 The Base64 encoded string. | ||||
|  */ | ||||
| export function base64ToArrayBuffer(base64: string) { | ||||
|     var binaryString = atob(base64); | ||||
|     var bytes = new Uint8Array(binaryString.length); | ||||
|     for (var i = 0; i < binaryString.length; i++) { | ||||
|         bytes[i] = binaryString.charCodeAt(i); | ||||
|     } | ||||
|     return bytes.buffer; | ||||
| } | ||||
|  | @ -2,48 +2,36 @@ import { beforeAll, describe, it, expect } from 'vitest'; | |||
| import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js'; | ||||
| import { setupTestApp } from '../../setup-tests.js'; | ||||
| import { getLearningObjectRepository } from '../../../src/data/repositories.js'; | ||||
| import example from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.js'; | ||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity.js'; | ||||
| import { expectToBeCorrectEntity } from '../../test-utils/expectations.js'; | ||||
| import { | ||||
|     testLearningObject01, | ||||
|     testLearningObject02, | ||||
|     testLearningObject03 | ||||
| } from "../../test_assets/content/learning-objects.testdata"; | ||||
| 
 | ||||
| describe('LearningObjectRepository', () => { | ||||
|     let learningObjectRepository: LearningObjectRepository; | ||||
| 
 | ||||
|     let exampleLearningObject: LearningObject; | ||||
| 
 | ||||
|     beforeAll(async () => { | ||||
|         await setupTestApp(); | ||||
|         learningObjectRepository = getLearningObjectRepository(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should be able to add a learning object to it without an error', async () => { | ||||
|         exampleLearningObject = example.createLearningObject(); | ||||
|         await learningObjectRepository.insert(exampleLearningObject); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return the learning object when queried by id', async () => { | ||||
|     it('should return a learning object when queried by id', async () => { | ||||
|         const result = await learningObjectRepository.findByIdentifier({ | ||||
|             hruid: exampleLearningObject.hruid, | ||||
|             language: exampleLearningObject.language, | ||||
|             version: exampleLearningObject.version, | ||||
|             hruid: testLearningObject01.hruid, | ||||
|             language: testLearningObject02.language, | ||||
|             version: testLearningObject03.version, | ||||
|         }); | ||||
|         expect(result).toBeInstanceOf(LearningObject); | ||||
|         expectToBeCorrectEntity( | ||||
|             { | ||||
|                 name: 'actual', | ||||
|                 entity: result!, | ||||
|             }, | ||||
|             { | ||||
|                 name: 'expected', | ||||
|                 entity: exampleLearningObject, | ||||
|             } | ||||
|         ); | ||||
|         expectToBeCorrectEntity(result!, testLearningObject01); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return null when non-existing version is queried', async () => { | ||||
|         const result = await learningObjectRepository.findByIdentifier({ | ||||
|             hruid: exampleLearningObject.hruid, | ||||
|             language: exampleLearningObject.language, | ||||
|             hruid: testLearningObject01.hruid, | ||||
|             language: testLearningObject01.language, | ||||
|             version: 100, | ||||
|         }); | ||||
|         expect(result).toBe(null); | ||||
|  | @ -52,21 +40,27 @@ describe('LearningObjectRepository', () => { | |||
|     let newerExample: LearningObject; | ||||
| 
 | ||||
|     it('should allow a learning object with the same id except a different version to be added', async () => { | ||||
|         newerExample = example.createLearningObject(); | ||||
|         newerExample.version = 10; | ||||
|         newerExample.title += ' (nieuw)'; | ||||
|         let testLearningObject01Newer = structuredClone(testLearningObject01); | ||||
|         testLearningObject01Newer.version = 10; | ||||
|         testLearningObject01Newer.title += " (nieuw)"; | ||||
|         testLearningObject01Newer.content = Buffer.from("This is the new content."); | ||||
|         newerExample = learningObjectRepository.create(testLearningObject01Newer); | ||||
|         await learningObjectRepository.save(newerExample); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return the newest version of the learning object when queried by only hruid and language', async () => { | ||||
|         const result = await learningObjectRepository.findLatestByHruidAndLanguage(newerExample.hruid, newerExample.language); | ||||
|         const result = await learningObjectRepository.findLatestByHruidAndLanguage( | ||||
|             newerExample.hruid, newerExample.language | ||||
|         ); | ||||
|         expect(result).toBeInstanceOf(LearningObject); | ||||
|         expect(result?.version).toBe(10); | ||||
|         expect(result?.title).toContain('(nieuw)'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return null when queried by non-existing hruid or language', async () => { | ||||
|         const result = await learningObjectRepository.findLatestByHruidAndLanguage('something_that_does_not_exist', exampleLearningObject.language); | ||||
|         const result = await learningObjectRepository.findLatestByHruidAndLanguage( | ||||
|             'something_that_does_not_exist', testLearningObject01.language | ||||
|         ); | ||||
|         expect(result).toBe(null); | ||||
|     }); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,10 +0,0 @@ | |||
| import { LearningObjectExample } from './learning-object-example'; | ||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; | ||||
| 
 | ||||
| export function createExampleLearningObjectWithAttachments(example: LearningObjectExample): LearningObject { | ||||
|     const learningObject = example.createLearningObject(); | ||||
|     for (const creationFn of Object.values(example.createAttachment)) { | ||||
|         learningObject.attachments.push(creationFn(learningObject)); | ||||
|     } | ||||
|     return learningObject; | ||||
| } | ||||
|  | @ -1,8 +0,0 @@ | |||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; | ||||
| import { Attachment } from '../../../src/entities/content/attachment.entity'; | ||||
| 
 | ||||
| interface LearningObjectExample { | ||||
|     createLearningObject: () => LearningObject; | ||||
|     createAttachment: Record<string, (owner: LearningObject) => Attachment>; | ||||
|     getHTMLRendering: () => string; | ||||
| } | ||||
|  | @ -1,2 +0,0 @@ | |||
| ::MC basic:: | ||||
| How are you? {} | ||||
|  | @ -1,3 +0,0 @@ | |||
| interface LearningPathExample { | ||||
|     createLearningPath: () => LearningPath; | ||||
| } | ||||
|  | @ -1,36 +0,0 @@ | |||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { LearningPathTransition } from '../../../src/entities/content/learning-path-transition.entity'; | ||||
| import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; | ||||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity'; | ||||
| 
 | ||||
| export function createLearningPathTransition( | ||||
|     node: LearningPathNode, | ||||
|     transitionNumber: number, | ||||
|     condition: string | null, | ||||
|     to: LearningPathNode | ||||
| ): LearningPathTransition { | ||||
|     const trans = new LearningPathTransition(); | ||||
|     trans.node = node; | ||||
|     trans.transitionNumber = transitionNumber; | ||||
|     trans.condition = condition || 'true'; | ||||
|     trans.next = to; | ||||
|     return trans; | ||||
| } | ||||
| 
 | ||||
| export function createLearningPathNode( | ||||
|     learningPath: LearningPath, | ||||
|     nodeNumber: number, | ||||
|     learningObjectHruid: string, | ||||
|     version: number, | ||||
|     language: Language, | ||||
|     startNode: boolean | ||||
| ): LearningPathNode { | ||||
|     const node = new LearningPathNode(); | ||||
|     node.learningPath = learningPath; | ||||
|     node.nodeNumber = nodeNumber; | ||||
|     node.learningObjectHruid = learningObjectHruid; | ||||
|     node.version = version; | ||||
|     node.language = language; | ||||
|     node.startNode = startNode; | ||||
|     return node; | ||||
| } | ||||
|  | @ -1,30 +0,0 @@ | |||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { envVars, getEnvVar } from '../../../src/util/envVars'; | ||||
| import { createLearningPathNode, createLearningPathTransition } from './learning-path-utils'; | ||||
| import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; | ||||
| 
 | ||||
| function createNodes(learningPath: LearningPath): LearningPathNode[] { | ||||
|     const nodes = [ | ||||
|         createLearningPathNode(learningPath, 0, 'u_pn_werkingnotebooks', 3, Language.Dutch, true), | ||||
|         createLearningPathNode(learningPath, 1, 'pn_werkingnotebooks2', 3, Language.Dutch, false), | ||||
|         createLearningPathNode(learningPath, 2, 'pn_werkingnotebooks3', 3, Language.Dutch, false), | ||||
|     ]; | ||||
|     nodes[0].transitions.push(createLearningPathTransition(nodes[0], 0, 'true', nodes[1])); | ||||
|     nodes[1].transitions.push(createLearningPathTransition(nodes[1], 0, 'true', nodes[2])); | ||||
|     return nodes; | ||||
| } | ||||
| 
 | ||||
| const example: LearningPathExample = { | ||||
|     createLearningPath: () => { | ||||
|         const path = new LearningPath(); | ||||
|         path.language = Language.Dutch; | ||||
|         path.hruid = `${getEnvVar(envVars.UserContentPrefix)}pn_werking`; | ||||
|         path.title = 'Werken met notebooks'; | ||||
|         path.description = 'Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiënt met de notebooks aan de slag?'; | ||||
|         path.nodes = createNodes(path); | ||||
|         return path; | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export default example; | ||||
|  | @ -1,80 +0,0 @@ | |||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import testMultipleChoiceExample from '../learning-objects/test-multiple-choice/test-multiple-choice-example'; | ||||
| import { dummyLearningObject } from '../learning-objects/dummy/dummy-learning-object-example'; | ||||
| import { createLearningPathNode, createLearningPathTransition } from './learning-path-utils'; | ||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; | ||||
| import { envVars, getEnvVar } from '../../../src/util/envVars'; | ||||
| 
 | ||||
| export interface ConditionTestLearningPathAndLearningObjects { | ||||
|     branchingObject: LearningObject; | ||||
|     extraExerciseObject: LearningObject; | ||||
|     finalObject: LearningObject; | ||||
|     learningPath: LearningPath; | ||||
| } | ||||
| 
 | ||||
| export function createConditionTestLearningPathAndLearningObjects(): ConditionTestLearningPathAndLearningObjects { | ||||
|     const learningPath = new LearningPath(); | ||||
|     learningPath.hruid = `${getEnvVar(envVars.UserContentPrefix)}test_conditions`; | ||||
|     learningPath.language = Language.English; | ||||
|     learningPath.title = 'Example learning path with conditional transitions'; | ||||
|     learningPath.description = 'This learning path was made for the purpose of testing conditional transitions'; | ||||
| 
 | ||||
|     const branchingLearningObject = testMultipleChoiceExample.createLearningObject(); | ||||
|     const extraExerciseLearningObject = dummyLearningObject( | ||||
|         'test_extra_exercise', | ||||
|         Language.English, | ||||
|         'Extra exercise (for students with difficulties)' | ||||
|     ).createLearningObject(); | ||||
|     const finalLearningObject = dummyLearningObject( | ||||
|         'test_final_learning_object', | ||||
|         Language.English, | ||||
|         'Final exercise (for everyone)' | ||||
|     ).createLearningObject(); | ||||
| 
 | ||||
|     const branchingNode = createLearningPathNode( | ||||
|         learningPath, | ||||
|         0, | ||||
|         branchingLearningObject.hruid, | ||||
|         branchingLearningObject.version, | ||||
|         branchingLearningObject.language, | ||||
|         true | ||||
|     ); | ||||
|     const extraExerciseNode = createLearningPathNode( | ||||
|         learningPath, | ||||
|         1, | ||||
|         extraExerciseLearningObject.hruid, | ||||
|         extraExerciseLearningObject.version, | ||||
|         extraExerciseLearningObject.language, | ||||
|         false | ||||
|     ); | ||||
|     const finalNode = createLearningPathNode( | ||||
|         learningPath, | ||||
|         2, | ||||
|         finalLearningObject.hruid, | ||||
|         finalLearningObject.version, | ||||
|         finalLearningObject.language, | ||||
|         false | ||||
|     ); | ||||
| 
 | ||||
|     const transitionToExtraExercise = createLearningPathTransition( | ||||
|         branchingNode, | ||||
|         0, | ||||
|         '$[?(@[0] == 0)]', // The answer to the first question was the first one, which says that it is difficult for the student to follow along.
 | ||||
|         extraExerciseNode | ||||
|     ); | ||||
|     const directTransitionToFinal = createLearningPathTransition(branchingNode, 1, '$[?(@[0] == 1)]', finalNode); | ||||
|     const transitionExtraExerciseToFinal = createLearningPathTransition(extraExerciseNode, 0, 'true', finalNode); | ||||
| 
 | ||||
|     branchingNode.transitions = [transitionToExtraExercise, directTransitionToFinal]; | ||||
|     extraExerciseNode.transitions = [transitionExtraExerciseToFinal]; | ||||
| 
 | ||||
|     learningPath.nodes = [branchingNode, extraExerciseNode, finalNode]; | ||||
| 
 | ||||
|     return { | ||||
|         branchingObject: branchingLearningObject, | ||||
|         finalObject: finalLearningObject, | ||||
|         extraExerciseObject: extraExerciseLearningObject, | ||||
|         learningPath: learningPath, | ||||
|     }; | ||||
| } | ||||
|  | @ -11,52 +11,45 @@ const IGNORE_PROPERTIES = ['parent']; | |||
|  * Checks if the actual entity from the database conforms to the entity that was added previously. | ||||
|  * @param actual The actual entity retrieved from the database | ||||
|  * @param expected The (previously added) entity we would expect to retrieve | ||||
|  * @param propertyPrefix Prefix to append to property in error messages. | ||||
|  */ | ||||
| export function expectToBeCorrectEntity<T extends object>(actual: { entity: T; name?: string }, expected: { entity: T; name?: string }): void { | ||||
|     if (!actual.name) { | ||||
|         actual.name = 'actual'; | ||||
|     } | ||||
|     if (!expected.name) { | ||||
|         expected.name = 'expected'; | ||||
|     } | ||||
|     for (const property in expected.entity) { | ||||
| export function expectToBeCorrectEntity<T extends object>( | ||||
|     actual: T, | ||||
|     expected: T, | ||||
|     propertyPrefix: string = "" | ||||
| ): void { | ||||
|     for (const property in expected) { | ||||
|         const prefixedProperty = propertyPrefix + property; | ||||
|         if ( | ||||
|             property in IGNORE_PROPERTIES && | ||||
|             expected.entity[property] !== undefined && // If we don't expect a certain value for a property, we assume it can be filled in by the database however it wants.
 | ||||
|             typeof expected.entity[property] !== 'function' // Functions obviously are not persisted via the database
 | ||||
|             expected[property] !== undefined && // If we don't expect a certain value for a property, we assume it can be filled in by the database however it wants.
 | ||||
|             typeof expected[property] !== 'function' // Functions obviously are not persisted via the database
 | ||||
|         ) { | ||||
|             if (!Object.prototype.hasOwnProperty.call(actual.entity, property)) { | ||||
|             if (!Object.prototype.hasOwnProperty.call(actual, property)) { | ||||
|                 throw new AssertionError({ | ||||
|                     message: `${expected.name} has defined property ${property}, but ${actual.name} is missing it.`, | ||||
|                     message: `Expected property ${prefixedProperty}, but it is missing.`, | ||||
|                 }); | ||||
|             } | ||||
|             if (typeof expected.entity[property] === 'boolean') { | ||||
|             if (typeof expected[property] === 'boolean') { | ||||
|                 // Sometimes, booleans get represented by numbers 0 and 1 in the objects actual from the database.
 | ||||
|                 if (Boolean(expected.entity[property]) !== Boolean(actual.entity[property])) { | ||||
|                 if (Boolean(expected[property]) !== Boolean(actual[property])) { | ||||
|                     throw new AssertionError({ | ||||
|                         message: `${property} was ${expected.entity[property]} in ${expected.name},
 | ||||
|                         but ${actual.entity[property]} (${Boolean(expected.entity[property])}) in ${actual.name}`,
 | ||||
|                         message: `Expected ${prefixedProperty} to be ${expected[property]},
 | ||||
|                         but was ${actual[property]} (${Boolean(expected[property])}).`,
 | ||||
|                     }); | ||||
|                 } | ||||
|             } else if (typeof expected.entity[property] !== typeof actual.entity[property]) { | ||||
|             } else if (typeof expected[property] !== typeof actual[property]) { | ||||
|                 throw new AssertionError({ | ||||
|                     message: `${property} has type ${typeof expected.entity[property]} in ${expected.name}, but type ${typeof actual.entity[property]} in ${actual.name}.`, | ||||
|                     message: `${prefixedProperty} was expected to have type ${typeof expected[property]},` | ||||
|                                 + `but had type ${typeof actual[property]}.`, | ||||
|                 }); | ||||
|             } else if (typeof expected.entity[property] === 'object') { | ||||
|                 expectToBeCorrectEntity( | ||||
|                     { | ||||
|                         name: actual.name + '.' + property, | ||||
|                         entity: actual.entity[property] as object, | ||||
|                     }, | ||||
|                     { | ||||
|                         name: expected.name + '.' + property, | ||||
|                         entity: expected.entity[property] as object, | ||||
|                     } | ||||
|                 ); | ||||
|             } else if (typeof expected[property] === 'object') { | ||||
|                 expectToBeCorrectEntity(actual[property] as object, expected[property] as object, property); | ||||
|             } else { | ||||
|                 if (expected.entity[property] !== actual.entity[property]) { | ||||
|                 if (expected[property] !== actual[property]) { | ||||
|                     throw new AssertionError({ | ||||
|                         message: `${property} was ${expected.entity[property]} in ${expected.name}, but ${actual.entity[property]} in ${actual.name}`, | ||||
|                         message: `${prefixedProperty} was expected to be ${expected[property]}, ` | ||||
|                                     + `but was ${actual[property]}.`, | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|  |  | |||
|  | @ -6,5 +6,5 @@ import path from 'node:path'; | |||
|  * @param relPath Path of the asset relative to the test-assets folder. | ||||
|  */ | ||||
| export function loadTestAsset(relPath: string): Buffer { | ||||
|     return fs.readFileSync(path.resolve(__dirname, `../test-assets/${relPath}`)); | ||||
|     return fs.readFileSync(path.resolve(__dirname, `../test_assets/${relPath}`)); | ||||
| } | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { LearningObjectExample } from '../learning-object-example'; | ||||
| import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { loadTestAsset } from '../../../test-utils/load-test-asset'; | ||||
| import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; | ||||
| import { envVars, getEnvVar } from '../../../../src/util/envVars'; | ||||
| import { LearningObject } from '../../../../../src/entities/content/learning-object.entity'; | ||||
| import { Language } from '@dwengo-1/common/dist/util/language'; | ||||
| import { loadTestAsset } from '../../../../test-utils/load-test-asset'; | ||||
| import { DwengoContentType } from '../../../../../src/services/learning-objects/processing/content-type'; | ||||
| import { envVars, getEnvVar } from '../../../../../src/util/envVars'; | ||||
| 
 | ||||
| /** | ||||
|  * Create a dummy learning object to be used in tests where multiple learning objects are needed (for example for use | ||||
| Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB | 
| Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB | 
|  | @ -1,12 +1,12 @@ | |||
| import { LearningObjectExample } from '../learning-object-example'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; | ||||
| import { loadTestAsset } from '../../../test-utils/load-test-asset'; | ||||
| import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; | ||||
| import { Attachment } from '../../../../src/entities/content/attachment.entity'; | ||||
| import { envVars, getEnvVar } from '../../../../src/util/envVars'; | ||||
| import { EducationalGoal } from '../../../../src/entities/content/educational-goal.entity'; | ||||
| import { ReturnValue } from '../../../../src/entities/content/return-value.entity'; | ||||
| import { Language } from '@dwengo-1/common/dist/util/language'; | ||||
| import { DwengoContentType } from '../../../../../src/services/learning-objects/processing/content-type'; | ||||
| import { loadTestAsset } from '../../../../test-utils/load-test-asset'; | ||||
| import { LearningObject } from '../../../../../src/entities/content/learning-object.entity'; | ||||
| import { Attachment } from '../../../../../src/entities/content/attachment.entity'; | ||||
| import { envVars, getEnvVar } from '../../../../../src/util/envVars'; | ||||
| import { EducationalGoal } from '../../../../../src/entities/content/educational-goal.entity'; | ||||
| import { ReturnValue } from '../../../../../src/entities/content/return-value.entity'; | ||||
| 
 | ||||
| const ASSETS_PREFIX = 'learning-objects/pn-werkingnotebooks/'; | ||||
| 
 | ||||
|  | @ -0,0 +1,2 @@ | |||
| ::MC basic:: | ||||
| Reflect on this learning path. What have you learned today? {} | ||||
|  | @ -1,7 +1,7 @@ | |||
| <div class="learning-object-gift"> | ||||
|     <div id="gift-q1" class="gift-question"> | ||||
|         <h2 id="gift-q1-title" class="gift-title">MC basic</h2> | ||||
|         <p id="gift-q1-stem" class="gift-stem">How are you?</p> | ||||
|         <p id="gift-q1-stem" class="gift-stem">Reflect on this learning path. What have you learned today?</p> | ||||
|         <textarea id="gift-q1-answer" class="gift-essay-answer"></textarea> | ||||
|     </div> | ||||
| </div> | ||||
|  | @ -1,9 +1,9 @@ | |||
| import { LearningObjectExample } from '../learning-object-example'; | ||||
| import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; | ||||
| import { loadTestAsset } from '../../../test-utils/load-test-asset'; | ||||
| import { envVars, getEnvVar } from '../../../../src/util/envVars'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; | ||||
| import { LearningObject } from '../../../../../src/entities/content/learning-object.entity'; | ||||
| import { loadTestAsset } from '../../../../test-utils/load-test-asset'; | ||||
| import { envVars, getEnvVar } from '../../../../../src/util/envVars'; | ||||
| import { Language } from '@dwengo-1/common/dist/util/language'; | ||||
| import { DwengoContentType } from '../../../../../src/services/learning-objects/processing/content-type'; | ||||
| 
 | ||||
| const example: LearningObjectExample = { | ||||
|     createLearningObject: () => { | ||||
|  | @ -1,5 +1,5 @@ | |||
| ::MC basic:: | ||||
| Are you following along well with the class?  { | ||||
| Are you following along well?  { | ||||
| 	~No, it's very difficult to follow along. | ||||
|     =Yes, no problem! | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| <div class="learning-object-gift"> | ||||
|     <div id="gift-q1" class="gift-question"> | ||||
|         <h2 id="gift-q1-title" class="gift-title">MC basic</h2> | ||||
|         <p id="gift-q1-stem" class="gift-stem">Are you following along well with the class?</p> | ||||
|         <p id="gift-q1-stem" class="gift-stem">Are you following along well?</p> | ||||
|         <div class="gift-choice-div"> | ||||
|             <input value="0" name="gift-q1-choices" id="gift-q1-choice-0" type="radio"> | ||||
|             <label for="gift-q1-choice-0">[object Object]</label> | ||||
|  | @ -1,9 +1,9 @@ | |||
| import { LearningObjectExample } from '../learning-object-example'; | ||||
| import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; | ||||
| import { loadTestAsset } from '../../../test-utils/load-test-asset'; | ||||
| import { envVars, getEnvVar } from '../../../../src/util/envVars'; | ||||
| import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { LearningObject } from '../../../../../src/entities/content/learning-object.entity'; | ||||
| import { loadTestAsset } from '../../../../test-utils/load-test-asset'; | ||||
| import { envVars, getEnvVar } from '../../../../../src/util/envVars'; | ||||
| import { DwengoContentType } from '../../../../../src/services/learning-objects/processing/content-type'; | ||||
| import { Language } from '@dwengo-1/common/dist/util/language'; | ||||
| 
 | ||||
| const example: LearningObjectExample = { | ||||
|     createLearningObject: () => { | ||||
|  | @ -1,15 +1,41 @@ | |||
| import { EntityManager } from '@mikro-orm/core'; | ||||
| import {EntityManager, RequiredEntityData} from '@mikro-orm/core'; | ||||
| import {LearningObject} from '../../../src/entities/content/learning-object.entity'; | ||||
| import {Language} from '@dwengo-1/common/util/language'; | ||||
| import {DwengoContentType} from '../../../src/services/learning-objects/processing/content-type'; | ||||
| import {ReturnValue} from '../../../src/entities/content/return-value.entity'; | ||||
| import {envVars, getEnvVar} from "../../../src/util/envVars"; | ||||
| import {loadTestAsset} from "../../test-utils/load-test-asset"; | ||||
| 
 | ||||
| export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | ||||
|     const returnValue: ReturnValue = new ReturnValue(); | ||||
|     returnValue.callbackSchema = ''; | ||||
|     returnValue.callbackUrl = ''; | ||||
| 
 | ||||
|     const learningObject01 = em.create(LearningObject, { | ||||
|     const learningObject01 = em.create(LearningObject, testLearningObject01); | ||||
|     const learningObject02 = em.create(LearningObject, testLearningObject02); | ||||
|     const learningObject03 = em.create(LearningObject, testLearningObject03); | ||||
|     const learningObject04 = em.create(LearningObject, testLearningObject04); | ||||
|     const learningObject05 = em.create(LearningObject, testLearningObject05); | ||||
| 
 | ||||
|     const learningObjectMultipleChoice = em.create(LearningObject, testLearningObjectMultipleChoice); | ||||
|     const learningObjectEssayQuestion= em.create(LearningObject, testLearningObjectEssayQuestion); | ||||
| 
 | ||||
|     const learningObjectPnNotebooks = em.create(LearningObject, testLearningObjectPnNotebooks); | ||||
| 
 | ||||
|     return [ | ||||
|         learningObject01, learningObject02, learningObject03, learningObject04, learningObject05, | ||||
|         learningObjectMultipleChoice, learningObjectEssayQuestion, learningObjectPnNotebooks | ||||
|     ]; | ||||
| } | ||||
| 
 | ||||
| export function createReturnValue(): ReturnValue { | ||||
|     const returnValue: ReturnValue = new ReturnValue(); | ||||
|     returnValue.callbackSchema = ''; | ||||
|     returnValue.callbackUrl = ''; | ||||
|     return returnValue; | ||||
| } | ||||
| 
 | ||||
| export const testLearningObject01: RequiredEntityData<LearningObject> = { | ||||
|     hruid: 'id01', | ||||
|     language: Language.English, | ||||
|     version: 1, | ||||
|  | @ -24,14 +50,14 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | |||
|     copyright: '', | ||||
|     license: '', | ||||
|     estimatedTime: 45, | ||||
|         returnValue: returnValue, | ||||
|     returnValue: createReturnValue(), | ||||
|     available: true, | ||||
|     contentLocation: '', | ||||
|     attachments: [], | ||||
|     content: Buffer.from("there's a shadow just behind me, shrouding every step i take, making every promise empty pointing every finger at me"), | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
|     const learningObject02 = em.create(LearningObject, { | ||||
| export const testLearningObject02: RequiredEntityData<LearningObject> = { | ||||
|     hruid: 'id02', | ||||
|     language: Language.English, | ||||
|     version: 1, | ||||
|  | @ -46,16 +72,16 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | |||
|     copyright: '', | ||||
|     license: '', | ||||
|     estimatedTime: 80, | ||||
|         returnValue: returnValue, | ||||
|     returnValue: createReturnValue(), | ||||
|     available: true, | ||||
|     contentLocation: '', | ||||
|     attachments: [], | ||||
|     content: Buffer.from( | ||||
|         "I've been crawling on my belly clearing out what could've been I've been wallowing in my own confused and insecure delusions" | ||||
|     ), | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
|     const learningObject03 = em.create(LearningObject, { | ||||
| export const testLearningObject03: RequiredEntityData<LearningObject> = { | ||||
|     hruid: 'id03', | ||||
|     language: Language.English, | ||||
|     version: 1, | ||||
|  | @ -70,7 +96,7 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | |||
|     copyright: '', | ||||
|     license: '', | ||||
|     estimatedTime: 55, | ||||
|         returnValue: returnValue, | ||||
|     returnValue: createReturnValue(), | ||||
|     available: true, | ||||
|     contentLocation: '', | ||||
|     attachments: [], | ||||
|  | @ -80,9 +106,9 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | |||
|          come back and see me later next patient please \ | ||||
|          send in another victim of industrial disease' | ||||
|     ), | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
|     const learningObject04 = em.create(LearningObject, { | ||||
| export const testLearningObject04: RequiredEntityData<LearningObject> = { | ||||
|     hruid: 'id04', | ||||
|     language: Language.English, | ||||
|     version: 1, | ||||
|  | @ -97,7 +123,7 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | |||
|     copyright: '', | ||||
|     license: '', | ||||
|     estimatedTime: 55, | ||||
|         returnValue: returnValue, | ||||
|     returnValue: createReturnValue(), | ||||
|     available: true, | ||||
|     contentLocation: '', | ||||
|     attachments: [], | ||||
|  | @ -107,9 +133,9 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | |||
|          I had the one-arm bandit fever \ | ||||
|          There was an arrow through my heart and my soul' | ||||
|     ), | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
|     const learningObject05 = em.create(LearningObject, { | ||||
| export const testLearningObject05: RequiredEntityData<LearningObject> = { | ||||
|     hruid: 'id05', | ||||
|     language: Language.English, | ||||
|     version: 1, | ||||
|  | @ -124,12 +150,103 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | |||
|     copyright: '', | ||||
|     license: '', | ||||
|     estimatedTime: 55, | ||||
|         returnValue: returnValue, | ||||
|     returnValue: createReturnValue(), | ||||
|     available: true, | ||||
|     contentLocation: '', | ||||
|     attachments: [], | ||||
|     content: Buffer.from('calling Elvis, is anybody home, calling elvis, I am here all alone'), | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
|     return [learningObject01, learningObject02, learningObject03, learningObject04, learningObject05]; | ||||
| export const testLearningObjectMultipleChoice: RequiredEntityData<LearningObject> = { | ||||
|     hruid: `${getEnvVar(envVars.UserContentPrefix)}test_multiple_choice`, | ||||
|     language: Language.English, | ||||
|     version: 1, | ||||
|     title: "Self-evaluation", | ||||
|     description: "Time to evaluate how well you understand what you've learned so far.", | ||||
|     keywords: ["test"], | ||||
|     teacherExclusive: false, | ||||
|     skosConcepts: [], | ||||
|     educationalGoals: [], | ||||
|     copyright: "Groep 1 SEL-2 2025", | ||||
|     license: "CC0", | ||||
|     difficulty: 1, | ||||
|     estimatedTime: 1, | ||||
|     attachments: [], | ||||
|     available: true, | ||||
|     targetAges: [10, 11, 12, 13, 14, 15, 16, 17, 18], | ||||
|     admins: [], | ||||
|     contentType: DwengoContentType.GIFT, | ||||
|     content: loadTestAsset('content/learning-object-resources/test-multiple-choice/content.txt'), | ||||
|     returnValue: { | ||||
|         callbackUrl: `%SUBMISSION%`, | ||||
|         callbackSchema: '["antwoord vraag 1"]', | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export const testLearningObjectEssayQuestion: RequiredEntityData<LearningObject> = { | ||||
|     hruid: `${getEnvVar(envVars.UserContentPrefix)}test_essay_question`, | ||||
|     language: Language.English, | ||||
|     version: 1, | ||||
|     title: "Reflection", | ||||
|     description: "Reflect on your learning progress.", | ||||
|     keywords: ["test"], | ||||
|     teacherExclusive: false, | ||||
|     skosConcepts: [], | ||||
|     educationalGoals: [], | ||||
|     copyright: "Groep 1 SEL-2 2025", | ||||
|     license: "CC0", | ||||
|     difficulty: 1, | ||||
|     estimatedTime: 1, | ||||
|     attachments: [], | ||||
|     available: true, | ||||
|     targetAges: [10, 11, 12, 13, 14, 15, 16, 17, 18], | ||||
|     admins: [], | ||||
|     contentType: DwengoContentType.GIFT, | ||||
|     content: loadTestAsset('content/learning-object-resources/test-essay/content.txt'), | ||||
|     returnValue: { | ||||
|         callbackUrl: `%SUBMISSION%`, | ||||
|         callbackSchema: '["antwoord vraag 1"]', | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export const testLearningObjectPnNotebooks: RequiredEntityData<LearningObject> = { | ||||
|     hruid: `${getEnvVar(envVars.UserContentPrefix)}pn_werkingnotebooks`, | ||||
|     version: 3, | ||||
|     language: Language.Dutch, | ||||
|     title: "Werken met notebooks", | ||||
|     description: "Leren werken met notebooks", | ||||
|     keywords: ["Python", "KIKS", "Wiskunde", "STEM", "AI"], | ||||
|     targetAges: [14, 15, 16, 17, 18], | ||||
|     admins: [], | ||||
|     copyright: "dwengo", | ||||
|     educationalGoals: [], | ||||
|     license: "dwengo", | ||||
|     contentType: DwengoContentType.TEXT_MARKDOWN, | ||||
|     difficulty: 3, | ||||
|     estimatedTime: 10, | ||||
|     uuid: "2adf9929-b424-4650-bf60-186f730d38ab", | ||||
|     teacherExclusive: false, | ||||
|     skosConcepts: [ | ||||
|         "http://ilearn.ilabt.imec.be/vocab/curr1/s-vaktaal", | ||||
|         "http://ilearn.ilabt.imec.be/vocab/curr1/s-digitale-media-en-toepassingen", | ||||
|         "http://ilearn.ilabt.imec.be/vocab/curr1/s-computers-en-systemen", | ||||
|     ], | ||||
|     attachments: [ | ||||
|         { | ||||
|             name: "dwengo.png", | ||||
|             mimeType: "image/png", | ||||
|             content: loadTestAsset("/content/learning-object-resources/pn-werkingnotebooks/dwengo.png") | ||||
|         }, | ||||
|         { | ||||
|             name: "Knop.png", | ||||
|             mimeType: "image/png", | ||||
|             content: loadTestAsset("/content/learning-object-resources/pn-werkingnotebooks/Knop.png") | ||||
|         } | ||||
|     ], | ||||
|     available: false, | ||||
|     content: loadTestAsset("/content/learning-object-resources/pn-werkingnotebooks/content.md"), | ||||
|     returnValue: { | ||||
|         callbackUrl: "%SUBMISSION%", | ||||
|         callbackSchema: "[]" | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,100 +1,237 @@ | |||
| import {EntityManager} from '@mikro-orm/core'; | ||||
| import {LearningPath} from '../../../src/entities/content/learning-path.entity'; | ||||
| import {Language} from '@dwengo-1/common/util/language'; | ||||
| import { LearningPathTransition } from '../../../src/entities/content/learning-path-transition.entity'; | ||||
| import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; | ||||
| import {mapToLearningPath} from "../../../src/services/learning-paths/learning-path-service"; | ||||
| import {envVars, getEnvVar} from "../../../src/util/envVars"; | ||||
| import {LearningPath as LearningPathDTO} from "@dwengo-1/common/interfaces/learning-content"; | ||||
| import { | ||||
|     testLearningObject01, testLearningObject02, testLearningObject03, testLearningObject04, testLearningObject05, | ||||
|     testLearningObjectEssayQuestion, | ||||
|     testLearningObjectMultipleChoice, testLearningObjectPnNotebooks | ||||
| } from "./learning-objects.testdata"; | ||||
| 
 | ||||
| export function makeTestLearningPaths(em: EntityManager): LearningPath[] { | ||||
|     const learningPathNode01: LearningPathNode = new LearningPathNode(); | ||||
|     const learningPathNode02: LearningPathNode = new LearningPathNode(); | ||||
|     const learningPathNode03: LearningPathNode = new LearningPathNode(); | ||||
|     const learningPathNode04: LearningPathNode = new LearningPathNode(); | ||||
|     const learningPathNode05: LearningPathNode = new LearningPathNode(); | ||||
| export function makeTestLearningPaths(_em: EntityManager): LearningPath[] { | ||||
|     const learningPath01 = mapToLearningPath(testLearningPath01, []); | ||||
|     const learningPath02 = mapToLearningPath(testLearningPath02, []); | ||||
| 
 | ||||
|     const transitions01: LearningPathTransition = new LearningPathTransition(); | ||||
|     const transitions02: LearningPathTransition = new LearningPathTransition(); | ||||
|     const transitions03: LearningPathTransition = new LearningPathTransition(); | ||||
|     const transitions04: LearningPathTransition = new LearningPathTransition(); | ||||
|     const transitions05: LearningPathTransition = new LearningPathTransition(); | ||||
|     const partiallyDatabasePartiallyDwengoApiLearningPath | ||||
|         = mapToLearningPath(testPartiallyDatabaseAndPartiallyDwengoApiLearningPath, []) | ||||
|     const learningPathWithConditions = mapToLearningPath(testLearningPathWithConditions, []) | ||||
| 
 | ||||
|     transitions01.condition = 'true'; | ||||
|     transitions01.next = learningPathNode02; | ||||
| 
 | ||||
|     transitions02.condition = 'true'; | ||||
|     transitions02.next = learningPathNode02; | ||||
| 
 | ||||
|     transitions03.condition = 'true'; | ||||
|     transitions03.next = learningPathNode04; | ||||
| 
 | ||||
|     transitions04.condition = 'true'; | ||||
|     transitions04.next = learningPathNode05; | ||||
| 
 | ||||
|     transitions05.condition = 'true'; | ||||
|     transitions05.next = learningPathNode05; | ||||
| 
 | ||||
|     learningPathNode01.instruction = ''; | ||||
|     learningPathNode01.language = Language.English; | ||||
|     learningPathNode01.learningObjectHruid = 'id01'; | ||||
|     learningPathNode01.startNode = true; | ||||
|     learningPathNode01.transitions = [transitions01]; | ||||
|     learningPathNode01.version = 1; | ||||
| 
 | ||||
|     learningPathNode02.instruction = ''; | ||||
|     learningPathNode02.language = Language.English; | ||||
|     learningPathNode02.learningObjectHruid = 'id02'; | ||||
|     learningPathNode02.startNode = false; | ||||
|     learningPathNode02.transitions = [transitions02]; | ||||
|     learningPathNode02.version = 1; | ||||
| 
 | ||||
|     learningPathNode03.instruction = ''; | ||||
|     learningPathNode03.language = Language.English; | ||||
|     learningPathNode03.learningObjectHruid = 'id03'; | ||||
|     learningPathNode03.startNode = true; | ||||
|     learningPathNode03.transitions = [transitions03]; | ||||
|     learningPathNode03.version = 1; | ||||
| 
 | ||||
|     learningPathNode04.instruction = ''; | ||||
|     learningPathNode04.language = Language.English; | ||||
|     learningPathNode04.learningObjectHruid = 'id04'; | ||||
|     learningPathNode04.startNode = false; | ||||
|     learningPathNode04.transitions = [transitions04]; | ||||
|     learningPathNode04.version = 1; | ||||
| 
 | ||||
|     learningPathNode05.instruction = ''; | ||||
|     learningPathNode05.language = Language.English; | ||||
|     learningPathNode05.learningObjectHruid = 'id05'; | ||||
|     learningPathNode05.startNode = false; | ||||
|     learningPathNode05.transitions = [transitions05]; | ||||
|     learningPathNode05.version = 1; | ||||
| 
 | ||||
|     const nodes01: LearningPathNode[] = [ | ||||
|         // LearningPathNode01,
 | ||||
|         // LearningPathNode02,
 | ||||
|     return [ | ||||
|         learningPath01, | ||||
|         learningPath02, | ||||
|         partiallyDatabasePartiallyDwengoApiLearningPath, | ||||
|         learningPathWithConditions | ||||
|     ]; | ||||
|     const learningPath01 = em.create(LearningPath, { | ||||
|         hruid: 'id01', | ||||
|         language: Language.English, | ||||
|         admins: [], | ||||
|         title: 'repertoire Tool', | ||||
|         description: 'all about Tool', | ||||
|         image: null, | ||||
|         nodes: nodes01, | ||||
|     }); | ||||
| 
 | ||||
|     const nodes02: LearningPathNode[] = [ | ||||
|         // LearningPathNode03,
 | ||||
|         // LearningPathNode04,
 | ||||
|         // LearningPathNode05,
 | ||||
|     ]; | ||||
|     const learningPath02 = em.create(LearningPath, { | ||||
|         hruid: 'id02', | ||||
|         language: Language.English, | ||||
|         admins: [], | ||||
|         title: 'repertoire Dire Straits', | ||||
|         description: 'all about Dire Straits', | ||||
|         image: null, | ||||
|         nodes: nodes02, | ||||
|     }); | ||||
| 
 | ||||
|     return [learningPath01, learningPath02]; | ||||
| } | ||||
| 
 | ||||
| const nowString = new Date().toString(); | ||||
| 
 | ||||
| export const testLearningPath01: LearningPathDTO = { | ||||
|     keywords: "test", | ||||
|     target_ages: [16, 17, 18], | ||||
|     hruid: "id01", | ||||
|     language: Language.English, | ||||
|     title: "repertoire Tool", | ||||
|     description: "all about Tool", | ||||
|     nodes: [ | ||||
|         { | ||||
|             learningobject_hruid: testLearningObject01.hruid, | ||||
|             language: testLearningObject01.language, | ||||
|             version: testLearningObject01.version, | ||||
|             start_node: true, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [ | ||||
|                 { | ||||
|                     next: { | ||||
|                         hruid: testLearningObject02.hruid, | ||||
|                         language: testLearningObject02.language, | ||||
|                         version: testLearningObject02.version | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             learningobject_hruid: testLearningObject02.hruid, | ||||
|             language: testLearningObject02.language, | ||||
|             version: testLearningObject02.version, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [] | ||||
|         } | ||||
|     ] | ||||
| }; | ||||
| 
 | ||||
| export const testLearningPath02: LearningPathDTO = { | ||||
|     keywords: "test", | ||||
|     target_ages: [16, 17, 18], | ||||
|     hruid: "id02", | ||||
|     language: Language.English, | ||||
|     title: "repertoire Dire Straits", | ||||
|     description: "all about Dire Straits", | ||||
|     nodes: [ | ||||
|         { | ||||
|             learningobject_hruid: testLearningObject03.hruid, | ||||
|             language: testLearningObject03.language, | ||||
|             version: testLearningObject03.version, | ||||
|             start_node: true, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [ | ||||
|                 { | ||||
|                     next: { | ||||
|                         hruid: testLearningObject04.hruid, | ||||
|                         language: testLearningObject04.language, | ||||
|                         version: testLearningObject04.version | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             learningobject_hruid: testLearningObject04.hruid, | ||||
|             language: testLearningObject04.language, | ||||
|             version: testLearningObject04.version, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [ | ||||
|                 { | ||||
|                     next: { | ||||
|                         hruid: testLearningObject05.hruid, | ||||
|                         language: testLearningObject05.language, | ||||
|                         version: testLearningObject05.version | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             learningobject_hruid: testLearningObject05.hruid, | ||||
|             language: testLearningObject05.language, | ||||
|             version: testLearningObject05.version, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [] | ||||
|         } | ||||
|     ] | ||||
| }; | ||||
| 
 | ||||
| export const testPartiallyDatabaseAndPartiallyDwengoApiLearningPath: LearningPathDTO = { | ||||
|     hruid: `${getEnvVar(envVars.UserContentPrefix)}pn_werking`, | ||||
|     title: "Werken met notebooks", | ||||
|     language: Language.Dutch, | ||||
|     description: 'Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiënt met de notebooks aan de slag?', | ||||
|     keywords: "Python KIKS Wiskunde STEM AI", | ||||
|     target_ages: [14, 15, 16, 17, 18], | ||||
|     nodes: [ | ||||
|         { | ||||
|             learningobject_hruid: testLearningObjectPnNotebooks.hruid, | ||||
|             language: testLearningObjectPnNotebooks.language, | ||||
|             version: testLearningObjectPnNotebooks.version, | ||||
|             start_node: true, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [ | ||||
|                 { | ||||
|                     default: true, | ||||
|                     next: { | ||||
|                         hruid: "pn_werkingnotebooks2", | ||||
|                         language: Language.Dutch, | ||||
|                         version: 3 | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             learningobject_hruid: "pn_werkingnotebooks2", | ||||
|             language: Language.Dutch, | ||||
|             version: 3, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [ | ||||
|                 { | ||||
|                     default: true, | ||||
|                     next: { | ||||
|                         hruid: "pn_werkingnotebooks3", | ||||
|                         language: Language.Dutch, | ||||
|                         version: 3 | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             learningobject_hruid: "pn_werkingnotebooks3", | ||||
|             language: Language.Dutch, | ||||
|             version: 3, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [] | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| 
 | ||||
| export const testLearningPathWithConditions: LearningPathDTO = { | ||||
|     hruid: `${getEnvVar(envVars.UserContentPrefix)}test_conditions`, | ||||
|     language: Language.English, | ||||
|     title: 'Example learning path with conditional transitions', | ||||
|     description: 'This learning path was made for the purpose of testing conditional transitions', | ||||
|     keywords: "test", | ||||
|     target_ages: [18, 19, 20, 21], | ||||
|     nodes: [ | ||||
|         { | ||||
|             learningobject_hruid: testLearningObjectMultipleChoice.hruid, | ||||
|             language: testLearningObjectMultipleChoice.language, | ||||
|             version: testLearningObjectMultipleChoice.version, | ||||
|             start_node: true, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [ | ||||
|                 { | ||||
|                     // If the answer to the first question was the first one (It's difficult to follow along):
 | ||||
|                     condition: '$[?(@[0] == 0)]', | ||||
|                     next: { | ||||
|                         //... we let the student do an extra exercise.
 | ||||
|                         hruid: testLearningObject01.hruid, | ||||
|                         language: testLearningObject01.language, | ||||
|                         version: testLearningObject01.version | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     // If the answer to the first question was the second one (I can follow along):
 | ||||
|                     condition: '$[?(@[0] == 1)]', | ||||
|                     next: { | ||||
|                         //... we let the student right through to the final question.
 | ||||
|                         hruid: testLearningObjectEssayQuestion.hruid, | ||||
|                         language: testLearningObjectEssayQuestion.language, | ||||
|                         version: testLearningObjectEssayQuestion.version | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             learningobject_hruid: testLearningObject01.hruid, | ||||
|             language: testLearningObject01.language, | ||||
|             version: testLearningObject01.version, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [ | ||||
|                 { | ||||
|                     default: true, | ||||
|                     next: { | ||||
|                         hruid: testLearningObjectEssayQuestion.hruid, | ||||
|                         language: testLearningObjectEssayQuestion.language, | ||||
|                         version: testLearningObjectEssayQuestion.version | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             learningobject_hruid: testLearningObjectEssayQuestion.hruid, | ||||
|             language: testLearningObjectEssayQuestion.language, | ||||
|             version: testLearningObjectEssayQuestion.version, | ||||
|             created_at: nowString, | ||||
|             updatedAt: nowString, | ||||
|             transitions: [] | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ export interface Transition { | |||
|         version: number; | ||||
|         language: string; | ||||
|     }; | ||||
|     condition?: string; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObjectIdentifierDTO { | ||||
|  | @ -18,7 +19,7 @@ export interface LearningObjectIdentifierDTO { | |||
| } | ||||
| 
 | ||||
| export interface LearningObjectNode { | ||||
|     _id: string; | ||||
|     _id?: string; | ||||
|     learningobject_hruid: string; | ||||
|     version: number; | ||||
|     language: Language; | ||||
|  | @ -30,20 +31,20 @@ export interface LearningObjectNode { | |||
| } | ||||
| 
 | ||||
| export interface LearningPath { | ||||
|     _id: string; | ||||
|     _id?: string; | ||||
|     language: string; | ||||
|     hruid: string; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     image?: string; // Image might be missing, so it's optional
 | ||||
|     num_nodes: number; | ||||
|     num_nodes_left: number; | ||||
|     num_nodes?: number; | ||||
|     num_nodes_left?: number; | ||||
|     nodes: LearningObjectNode[]; | ||||
|     keywords: string; | ||||
|     target_ages: number[]; | ||||
|     min_age: number; | ||||
|     max_age: number; | ||||
|     __order: number; | ||||
|     min_age?: number; | ||||
|     max_age?: number; | ||||
|     __order?: number; | ||||
| } | ||||
| 
 | ||||
| export interface LearningPathIdentifier { | ||||
|  | @ -62,8 +63,8 @@ export interface ReturnValue { | |||
| } | ||||
| 
 | ||||
| export interface LearningObjectMetadata { | ||||
|     _id: string; | ||||
|     uuid: string; | ||||
|     _id?: string; | ||||
|     uuid?: string; | ||||
|     hruid: string; | ||||
|     version: number; | ||||
|     language: Language; | ||||
|  | @ -84,7 +85,7 @@ export interface LearningObjectMetadata { | |||
| 
 | ||||
| export interface FilteredLearningObject { | ||||
|     key: string; | ||||
|     _id: string; | ||||
|     _id?: string; | ||||
|     uuid: string; | ||||
|     version: number; | ||||
|     title: string; | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger