fix(backend): Fouten in isTransitionPossible en het opzetten van de testdata verbeterd.
This commit is contained in:
		
							parent
							
								
									b539c28d8c
								
							
						
					
					
						commit
						fc46e79d05
					
				
					 10 changed files with 249 additions and 68 deletions
				
			
		|  | @ -15,8 +15,8 @@ export class Submission { | |||
|     }) | ||||
|     learningObjectLanguage!: Language; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'string' }) | ||||
|     learningObjectVersion: string = '1'; | ||||
|     @PrimaryKey({ type: 'numeric' }) | ||||
|     learningObjectVersion: number = 1; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'integer' }) | ||||
|     submissionNumber!: number; | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ export class LearningObject { | |||
|     teacherExclusive: boolean = false; | ||||
| 
 | ||||
|     @Property({ type: 'array' }) | ||||
|     skosConcepts!: string[]; | ||||
|     skosConcepts: string[] = []; | ||||
| 
 | ||||
|     @Embedded({ | ||||
|         entity: () => EducationalGoal, | ||||
|  | @ -64,8 +64,8 @@ export class LearningObject { | |||
|     @Property({ type: 'smallint', nullable: true }) | ||||
|     difficulty?: number; | ||||
| 
 | ||||
|     @Property({ type: 'integer' }) | ||||
|     estimatedTime!: number; | ||||
|     @Property({ type: 'integer', nullable: true }) | ||||
|     estimatedTime?: number; | ||||
| 
 | ||||
|     @Embedded({ | ||||
|         entity: () => ReturnValue, | ||||
|  |  | |||
|  | @ -97,14 +97,26 @@ async function convertNodes( | |||
|                 learningobject_hruid: node.learningObjectHruid, | ||||
|                 version: learningObject.version, | ||||
|                 transitions: node.transitions | ||||
|                     .filter((trans) => !personalizedFor || isTransitionPossible(trans, lastSubmission)) // If we want a personalized learning path, remove all transitions that aren't possible.
 | ||||
|                     .map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition
 | ||||
|                     .filter((trans) => | ||||
|                         !personalizedFor || isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content))  // If we want a personalized learning path, remove all transitions that aren't possible.
 | ||||
|                     ).map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition
 | ||||
|             }; | ||||
|         }) | ||||
|         .toArray(); | ||||
|     return await Promise.all(nodesPromise); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Helper method to convert a json string to an object, or null if it is undefined. | ||||
|  */ | ||||
| function optionalJsonStringToObject(jsonString?: string): object | null { | ||||
|     if (!jsonString) { | ||||
|         return null; | ||||
|     } else { | ||||
|         return JSON.parse(jsonString); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Helper function which converts a transition in the database representation to a transition in the representation | ||||
|  * the Dwengo API uses. | ||||
|  |  | |||
|  | @ -36,5 +36,6 @@ export function isTransitionPossible(transition: LearningPathTransition, submitt | |||
|     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; | ||||
|     const match = JSONPath({ path: transition.condition, json: {submission: submitted} }) | ||||
|     return match.length === 1; | ||||
| } | ||||
|  |  | |||
|  | @ -2,7 +2,11 @@ import { beforeAll, describe, expect, it } from 'vitest'; | |||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; | ||||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity'; | ||||
| import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories'; | ||||
| import { | ||||
|     getLearningObjectRepository, | ||||
|     getLearningPathRepository, | ||||
|     getStudentRepository, getSubmissionRepository | ||||
| } from '../../../src/data/repositories'; | ||||
| import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example'; | ||||
| import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; | ||||
| import databaseLearningPathProvider from '../../../src/services/learning-paths/database-learning-path-provider'; | ||||
|  | @ -10,6 +14,12 @@ import { expectToBeCorrectLearningPath } from '../../test-utils/expectations'; | |||
| import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository'; | ||||
| import learningObjectService from '../../../src/services/learning-objects/learning-object-service'; | ||||
| import { Language } from '../../../src/entities/content/language'; | ||||
| import { | ||||
|     ConditionTestLearningPathAndLearningObjects, | ||||
|     createConditionTestLearningPathAndLearningObjects | ||||
| } from "../../test-assets/learning-paths/test-conditions-example"; | ||||
| import {Student} from "../../../src/entities/users/student.entity"; | ||||
| import {LearningObjectNode, LearningPathResponse} from "../../../src/interfaces/learning-content"; | ||||
| 
 | ||||
| async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { | ||||
|     const learningObjectRepo = getLearningObjectRepository(); | ||||
|  | @ -21,13 +31,85 @@ async function initExampleData(): Promise<{ learningObject: LearningObject; lear | |||
|     return { learningObject, learningPath }; | ||||
| } | ||||
| 
 | ||||
| async function initPersonalizationTestData(): Promise<{ | ||||
|     learningContent: ConditionTestLearningPathAndLearningObjects, | ||||
|     studentA: Student, | ||||
|     studentB: Student | ||||
| }> { | ||||
|     const studentRepo = getStudentRepository(); | ||||
|     const submissionRepo = getSubmissionRepository(); | ||||
|     const learningPathRepo = getLearningPathRepository(); | ||||
|     const learningObjectRepo = getLearningObjectRepository(); | ||||
|     const learningContent = createConditionTestLearningPathAndLearningObjects(); | ||||
|     await learningObjectRepo.save(learningContent.branchingObject); | ||||
|     await learningObjectRepo.save(learningContent.finalObject); | ||||
|     await learningObjectRepo.save(learningContent.extraExerciseObject); | ||||
|     await learningPathRepo.save(learningContent.learningPath); | ||||
| 
 | ||||
|     console.log(await getSubmissionRepository().findAll({})); | ||||
| 
 | ||||
|     const studentA = studentRepo.create({ | ||||
|         username: "student_a", | ||||
|         firstName: "Aron", | ||||
|         lastName: "Student" | ||||
|     }); | ||||
|     await studentRepo.save(studentA); | ||||
|     const submissionA = submissionRepo.create({ | ||||
|         learningObjectHruid: learningContent.branchingObject.hruid, | ||||
|         learningObjectLanguage: learningContent.branchingObject.language, | ||||
|         learningObjectVersion: learningContent.branchingObject.version, | ||||
|         submissionNumber: 0, | ||||
|         submitter: studentA, | ||||
|         submissionTime: new Date(), | ||||
|         content: '[0]' | ||||
|     }); | ||||
|     await submissionRepo.save(submissionA); | ||||
| 
 | ||||
|     const studentB = studentRepo.create({ | ||||
|         username: "student_b", | ||||
|         firstName: "Bill", | ||||
|         lastName: "Student" | ||||
|     }); | ||||
|     await studentRepo.save(studentB); | ||||
|     const submissionB = submissionRepo.create({ | ||||
|         learningObjectHruid: learningContent.branchingObject.hruid, | ||||
|         learningObjectLanguage: learningContent.branchingObject.language, | ||||
|         learningObjectVersion: learningContent.branchingObject.version, | ||||
|         submissionNumber: 1, | ||||
|         submitter: studentB, | ||||
|         submissionTime: new Date(), | ||||
|         content: '[1]' | ||||
|     }); | ||||
|     await submissionRepo.save(submissionB); | ||||
| 
 | ||||
|     return { | ||||
|         learningContent: learningContent, | ||||
|         studentA: studentA, | ||||
|         studentB: studentB, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function expectBranchingObjectNode(result: LearningPathResponse, persTestData: { | ||||
|     learningContent: ConditionTestLearningPathAndLearningObjects; | ||||
|     studentA: Student; | ||||
|     studentB: Student | ||||
| }): LearningObjectNode { | ||||
|     let branchingObjectMatches = result.data![0].nodes.filter( | ||||
|         it => it.learningobject_hruid === persTestData.learningContent.branchingObject.hruid | ||||
|     ); | ||||
|     expect(branchingObjectMatches.length).toBe(1); | ||||
|     return branchingObjectMatches[0]; | ||||
| } | ||||
| 
 | ||||
| describe('DatabaseLearningPathProvider', () => { | ||||
|     let learningObjectRepo: LearningObjectRepository; | ||||
|     let example: { learningObject: LearningObject; learningPath: LearningPath }; | ||||
|     let persTestData: { learningContent: ConditionTestLearningPathAndLearningObjects, studentA: Student, studentB: Student } | ||||
| 
 | ||||
|     beforeAll(async () => { | ||||
|         await setupTestApp(); | ||||
|         example = await initExampleData(); | ||||
|         persTestData = await initPersonalizationTestData(); | ||||
|         learningObjectRepo = getLearningObjectRepository(); | ||||
|     }); | ||||
| 
 | ||||
|  | @ -55,6 +137,60 @@ describe('DatabaseLearningPathProvider', () => { | |||
| 
 | ||||
|             expectToBeCorrectLearningPath(result.data![0], example.learningPath, learningObjectsOnPath); | ||||
|         }); | ||||
|         it("returns the correct personalized learning path", async () => { | ||||
|             // For student A:
 | ||||
|             let result = await databaseLearningPathProvider.fetchLearningPaths( | ||||
|                 [persTestData.learningContent.learningPath.hruid], | ||||
|                 persTestData.learningContent.learningPath.language, | ||||
|                 'the source', | ||||
|                 {type: 'student', student: persTestData.studentA} | ||||
|             ); | ||||
|             expect(result.success).toBeTruthy(); | ||||
|             expect(result.data?.length).toBe(1); | ||||
| 
 | ||||
|             // There should be exactly one branching object
 | ||||
|             let branchingObject = expectBranchingObjectNode(result, persTestData); | ||||
| 
 | ||||
|             expect( | ||||
|                 branchingObject | ||||
|                     .transitions | ||||
|                     .filter(it => it.next.hruid === persTestData.learningContent.finalObject.hruid) | ||||
|                     .length | ||||
|             ).toBe(0); // studentA picked the first option, therefore, there should be no direct path to the final object.
 | ||||
|             expect( | ||||
|                 branchingObject | ||||
|                     .transitions | ||||
|                     .filter(it => it.next.hruid === persTestData.learningContent.extraExerciseObject.hruid) | ||||
|                     .length | ||||
|             ).toBe(1); // There should however be a path to the extra exercise object.
 | ||||
| 
 | ||||
|             // For student B:
 | ||||
|             result = await databaseLearningPathProvider.fetchLearningPaths( | ||||
|                 [persTestData.learningContent.learningPath.hruid], | ||||
|                 persTestData.learningContent.learningPath.language, | ||||
|                 'the source', | ||||
|                 {type: 'student', student: persTestData.studentB} | ||||
|             ); | ||||
|             expect(result.success).toBeTruthy(); | ||||
|             expect(result.data?.length).toBe(1); | ||||
| 
 | ||||
|             // There should still be exactly one branching object
 | ||||
|             branchingObject = expectBranchingObjectNode(result, persTestData); | ||||
| 
 | ||||
|             // However, now the student picks the other option.
 | ||||
|             expect( | ||||
|                 branchingObject | ||||
|                     .transitions | ||||
|                     .filter(it => it.next.hruid === persTestData.learningContent.finalObject.hruid) | ||||
|                     .length | ||||
|             ).toBe(1); // studentB picked the second option, therefore, there should be a direct path to the final object.
 | ||||
|             expect( | ||||
|                 branchingObject | ||||
|                     .transitions | ||||
|                     .filter(it => it.next.hruid === persTestData.learningContent.extraExerciseObject.hruid) | ||||
|                     .length | ||||
|             ).toBe(0); // There should not be a path anymore to the extra exercise object.
 | ||||
|         }); | ||||
|         it('returns a non-successful response if a non-existing learning path is queried', async () => { | ||||
|             const result = await databaseLearningPathProvider.fetchLearningPaths( | ||||
|                 [example.learningPath.hruid], | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import { LearningObject } from '../../../../src/entities/content/learning-object | |||
| import { Language } from '../../../../src/entities/content/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 | ||||
|  | @ -12,13 +13,17 @@ export function dummyLearningObject(hruid: string, language: Language, title: st | |||
|     return { | ||||
|         createLearningObject: () => { | ||||
|             const learningObject = new LearningObject(); | ||||
|             learningObject.hruid = hruid; | ||||
|             learningObject.hruid = getEnvVar(EnvVars.UserContentPrefix) + hruid; | ||||
|             learningObject.language = language; | ||||
|             learningObject.version = 1; | ||||
|             learningObject.title = title; | ||||
|             learningObject.description = 'Just a dummy learning object for testing purposes'; | ||||
|             learningObject.contentType = DwengoContentType.TEXT_PLAIN; | ||||
|             learningObject.content = Buffer.from('Dummy content'); | ||||
|             learningObject.returnValue = { | ||||
|                 callbackUrl: `/learningObject/${hruid}/submissions`, | ||||
|                 callbackSchema: "[]" | ||||
|             } | ||||
|             return learningObject; | ||||
|         }, | ||||
|         createAttachment: {}, | ||||
|  |  | |||
|  | @ -14,6 +14,10 @@ const example: LearningObjectExample = { | |||
|         learningObject.title = 'Essay question for testing'; | ||||
|         learningObject.description = 'This essay question was only created for testing purposes.'; | ||||
|         learningObject.contentType = DwengoContentType.GIFT; | ||||
|         learningObject.returnValue = { | ||||
|             callbackUrl: `/learningObject/${learningObject.hruid}/submissions`, | ||||
|             callbackSchema: '["antwoord vraag 1"]' | ||||
|         } | ||||
|         learningObject.content = loadTestAsset('learning-objects/test-essay/content.txt'); | ||||
|         return learningObject; | ||||
|     }, | ||||
|  |  | |||
|  | @ -14,6 +14,10 @@ const example: LearningObjectExample = { | |||
|         learningObject.title = 'Multiple choice question for testing'; | ||||
|         learningObject.description = 'This multiple choice question was only created for testing purposes.'; | ||||
|         learningObject.contentType = DwengoContentType.GIFT; | ||||
|         learningObject.returnValue = { | ||||
|             callbackUrl: `/learningObject/${learningObject.hruid}/submissions`, | ||||
|             callbackSchema: '["antwoord vraag 1"]' | ||||
|         } | ||||
|         learningObject.content = loadTestAsset('learning-objects/test-multiple-choice/content.txt'); | ||||
|         return learningObject; | ||||
|     }, | ||||
|  |  | |||
|  | @ -3,66 +3,84 @@ import { Language } from '../../../src/entities/content/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 type ConditionTestLearningPathAndLearningObjects = { | ||||
|     branchingObject: LearningObject, | ||||
|     extraExerciseObject: LearningObject, | ||||
|     finalObject: LearningObject, | ||||
|     learningPath: LearningPath | ||||
| }; | ||||
| 
 | ||||
| export function createConditionTestLearningPathAndLearningObjects(){ | ||||
|     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, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| const example: LearningPathExample = { | ||||
|     createLearningPath: () => { | ||||
|         const learningPath = new LearningPath(); | ||||
|         learningPath.hruid = '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 learningPath; | ||||
|         return createConditionTestLearningPathAndLearningObjects().learningPath; | ||||
|     }, | ||||
| }; | ||||
|  |  | |||
|  | @ -4,5 +4,6 @@ export default defineConfig({ | |||
|     test: { | ||||
|         environment: 'node', | ||||
|         globals: true, | ||||
|         testTimeout: 10000, | ||||
|     }, | ||||
| }); | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger