From a30c4d0d32d6d69a69dcc034d4d93bca97338371 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 11 Mar 2025 04:08:32 +0100 Subject: [PATCH] =?UTF-8?q?feat(backend):=20LearningPathPersonalizingServi?= =?UTF-8?q?ce=20ge=C3=AFmplementeerd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 16 +- backend/src/controllers/learning-objects.ts | 2 +- backend/src/exceptions.ts | 1 + .../learning-path-personalizing-service.ts | 66 +++++++- package-lock.json | 160 ++++++------------ 5 files changed, 129 insertions(+), 116 deletions(-) diff --git a/backend/package.json b/backend/package.json index 2d9ea005..1e589752 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,24 +18,24 @@ "@mikro-orm/postgresql": "^6.4.6", "@mikro-orm/reflection": "^6.4.6", "@mikro-orm/sqlite": "6.4.6", + "@types/cors": "^2.8.17", "@types/js-yaml": "^4.0.9", "axios": "^1.8.2", + "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^5.0.1", + "express-jwt": "^8.5.1", "gift-pegjs": "^1.0.2", "isomorphic-dompurify": "^2.22.0", - "express-jwt": "^8.5.1", - "jwks-rsa": "^3.1.0", - "uuid": "^11.1.0", "js-yaml": "^4.1.0", - "marked": "^15.0.7", - "uuid": "^11.1.0", + "jsonpath-plus": "^10.3.0", + "jwks-rsa": "^3.1.0", "loki-logger-ts": "^1.0.2", + "marked": "^15.0.7", "response-time": "^2.3.3", + "uuid": "^11.1.0", "winston": "^3.17.0", - "winston-loki": "^6.1.3", - "cors": "^2.8.5", - "@types/cors": "^2.8.17" + "winston-loki": "^6.1.3" }, "devDependencies": { "@mikro-orm/cli": "^6.4.6", diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index 0445d9f1..ece16057 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -15,7 +15,7 @@ function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIde return { hruid: req.params.hruid as string, language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language, - version: req.query.version as string + version: parseInt(req.query.version as string) }; } diff --git a/backend/src/exceptions.ts b/backend/src/exceptions.ts index 8e2c886c..e93a6c93 100644 --- a/backend/src/exceptions.ts +++ b/backend/src/exceptions.ts @@ -24,6 +24,7 @@ export class UnauthorizedException extends Error { */ export class ForbiddenException extends Error { status = 403; + constructor(message: string = 'Forbidden') { super(message); } diff --git a/backend/src/services/learning-paths/learning-path-personalizing-service.ts b/backend/src/services/learning-paths/learning-path-personalizing-service.ts index 2341cc60..b7d1fe06 100644 --- a/backend/src/services/learning-paths/learning-path-personalizing-service.ts +++ b/backend/src/services/learning-paths/learning-path-personalizing-service.ts @@ -1,5 +1,69 @@ -const learningPathPersonalizingService = { +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 { + const submissionRepo = getSubmissionRepository(); + const learningObjectId: LearningObjectIdentifier = { + hruid: node.learningObjectHruid, + language: node.language, + version: node.version + }; + let lastSubmission: Submission | null; + if (pathFor.group) { + lastSubmission = await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor.group); + } 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!"); + } + return lastSubmission; +} + +function transitionPossible(transition: LearningPathTransition, submitted: object | null): boolean { + 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; +} + +/** + * 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 { + let trajectory: LearningPathNode[] = []; + + // Always start with the start node. + 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)); + + 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); + } + } + } }; export default learningPathPersonalizingService; diff --git a/package-lock.json b/package-lock.json index 914e100b..c8d5c035 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,17 +36,16 @@ "@types/js-yaml": "^4.0.9", "axios": "^1.8.2", "cors": "^2.8.5", - "@mikro-orm/sqlite": "6.4.6", - "@types/js-yaml": "^4.0.9", "dotenv": "^16.4.7", "express": "^5.0.1", + "express-jwt": "^8.5.1", "gift-pegjs": "^1.0.2", "isomorphic-dompurify": "^2.22.0", - "express-jwt": "^8.5.1", "js-yaml": "^4.1.0", - "marked": "^15.0.7", + "jsonpath-plus": "^10.3.0", "jwks-rsa": "^3.1.0", "loki-logger-ts": "^1.0.2", + "marked": "^15.0.7", "response-time": "^2.3.3", "uuid": "^11.1.0", "winston": "^3.17.0", @@ -155,7 +154,6 @@ }, "node_modules/@asamuzakjp/css-color": { "version": "2.8.3", - "dev": true, "license": "MIT", "dependencies": { "@csstools/css-calc": "^2.1.1", @@ -623,7 +621,6 @@ }, "node_modules/@csstools/color-helpers": { "version": "5.0.1", - "dev": true, "funding": [ { "type": "github", @@ -641,7 +638,6 @@ }, "node_modules/@csstools/css-calc": { "version": "2.1.1", - "dev": true, "funding": [ { "type": "github", @@ -663,7 +659,6 @@ }, "node_modules/@csstools/css-color-parser": { "version": "3.0.7", - "dev": true, "funding": [ { "type": "github", @@ -689,7 +684,6 @@ }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.4", - "dev": true, "funding": [ { "type": "github", @@ -710,7 +704,6 @@ }, "node_modules/@csstools/css-tokenizer": { "version": "3.0.3", - "dev": true, "funding": [ { "type": "github", @@ -969,44 +962,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@intlify/core-base": { - "version": "10.0.5", - "license": "MIT", - "dependencies": { - "@intlify/message-compiler": "10.0.5", - "@intlify/shared": "10.0.5" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" - } - }, - "node_modules/@intlify/message-compiler": { - "version": "10.0.5", - "license": "MIT", - "dependencies": { - "@intlify/shared": "10.0.5", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" - } - }, - "node_modules/@intlify/shared": { - "version": "10.0.5", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "dev": true, @@ -1084,6 +1039,30 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@mikro-orm/cli": { "version": "6.4.6", "dev": true, @@ -1721,7 +1700,6 @@ }, "node_modules/@types/http-errors": { "version": "2.0.4", - "dev": true, "license": "MIT" }, "node_modules/@types/js-yaml": { @@ -1755,7 +1733,6 @@ }, "node_modules/@types/mime": { "version": "1.3.5", - "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -1766,7 +1743,6 @@ }, "node_modules/@types/node": { "version": "22.13.4", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -1774,12 +1750,10 @@ }, "node_modules/@types/qs": { "version": "6.9.18", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", - "dev": true, "license": "MIT" }, "node_modules/@types/response-time": { @@ -1795,7 +1769,6 @@ }, "node_modules/@types/send": { "version": "0.17.4", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -1804,7 +1777,6 @@ }, "node_modules/@types/serve-static": { "version": "1.15.7", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -2498,7 +2470,6 @@ }, "node_modules/agent-base": { "version": "7.1.3", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -2636,7 +2607,6 @@ "version": "1.8.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", - "version": "1.8.1", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -3135,7 +3105,6 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/color-string": { @@ -3315,7 +3284,6 @@ }, "node_modules/cssstyle": { "version": "4.2.1", - "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^2.8.2", @@ -3331,7 +3299,6 @@ }, "node_modules/data-urls": { "version": "5.0.0", - "dev": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", @@ -3367,7 +3334,6 @@ }, "node_modules/decimal.js": { "version": "10.5.0", - "dev": true, "license": "MIT" }, "node_modules/decompress-response": { @@ -4804,7 +4770,6 @@ }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", - "dev": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^3.1.1" @@ -4845,7 +4810,6 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -4857,7 +4821,6 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -5097,7 +5060,6 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/is-promise": { @@ -5250,7 +5212,6 @@ }, "node_modules/jsdom": { "version": "26.0.0", - "dev": true, "license": "MIT", "dependencies": { "cssstyle": "^4.2.1", @@ -5289,12 +5250,20 @@ }, "node_modules/jsdom/node_modules/xml-name-validator": { "version": "5.0.0", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18" } }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "dev": true, @@ -5350,6 +5319,24 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonpath-plus": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", + "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -5663,7 +5650,6 @@ }, "node_modules/lru-cache": { "version": "10.4.3", - "dev": true, "license": "ISC" }, "node_modules/lru-memoizer": { @@ -6399,7 +6385,6 @@ }, "node_modules/nwsapi": { "version": "2.2.16", - "dev": true, "license": "MIT" }, "node_modules/object-assign": { @@ -6579,7 +6564,6 @@ }, "node_modules/parse5": { "version": "7.2.1", - "dev": true, "license": "MIT", "dependencies": { "entities": "^4.5.0" @@ -7049,7 +7033,6 @@ }, "node_modules/punycode": { "version": "2.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7354,7 +7337,6 @@ }, "node_modules/rrweb-cssom": { "version": "0.8.0", - "dev": true, "license": "MIT" }, "node_modules/run-applescript": { @@ -7422,7 +7404,6 @@ }, "node_modules/saxes": { "version": "6.0.0", - "dev": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -8042,7 +8023,6 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", - "dev": true, "license": "MIT" }, "node_modules/synckit": { @@ -8170,7 +8150,6 @@ }, "node_modules/tldts": { "version": "6.1.77", - "dev": true, "license": "MIT", "dependencies": { "tldts-core": "^6.1.77" @@ -8181,7 +8160,6 @@ }, "node_modules/tldts-core": { "version": "6.1.77", - "dev": true, "license": "MIT" }, "node_modules/to-regex-range": { @@ -8211,7 +8189,6 @@ }, "node_modules/tough-cookie": { "version": "5.1.1", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" @@ -8222,7 +8199,6 @@ }, "node_modules/tr46": { "version": "5.0.0", - "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -8416,7 +8392,6 @@ }, "node_modules/undici-types": { "version": "6.20.0", - "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -8913,24 +8888,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/vue-i18n": { - "version": "10.0.5", - "license": "MIT", - "dependencies": { - "@intlify/core-base": "10.0.5", - "@intlify/shared": "10.0.5", - "@vue/devtools-api": "^6.5.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" - }, - "peerDependencies": { - "vue": "^3.0.0" - } - }, "node_modules/vue-router": { "version": "4.5.0", "license": "MIT", @@ -8989,7 +8946,6 @@ }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", - "dev": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -9000,7 +8956,6 @@ }, "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { "version": "5.0.0", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -9008,7 +8963,6 @@ }, "node_modules/webidl-conversions": { "version": "7.0.0", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -9016,7 +8970,6 @@ }, "node_modules/whatwg-encoding": { "version": "3.1.1", - "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -9027,7 +8980,6 @@ }, "node_modules/whatwg-encoding/node_modules/iconv-lite": { "version": "0.6.3", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -9038,7 +8990,6 @@ }, "node_modules/whatwg-mimetype": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -9046,7 +8997,6 @@ }, "node_modules/whatwg-url": { "version": "14.1.1", - "dev": true, "license": "MIT", "dependencies": { "tr46": "^5.0.0", @@ -9289,7 +9239,6 @@ }, "node_modules/ws": { "version": "8.18.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -9317,7 +9266,6 @@ }, "node_modules/xmlchars": { "version": "2.2.0", - "dev": true, "license": "MIT" }, "node_modules/xtend": {