Merge remote-tracking branch 'origin/dev' into feat/user-homepage

# Conflicts:
#	package-lock.json
This commit is contained in:
Gabriellvl 2025-03-22 21:25:19 +01:00
commit 4698311062
7 changed files with 674 additions and 2076 deletions

19
.github/workflows/deployment.yml vendored Normal file
View file

@ -0,0 +1,19 @@
name: Deployment
on:
push:
branches:
- main
jobs:
docker:
name: Deploy with docker
runs-on: [self-hosted, Linux, X64]
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Start docker
run: docker compose -f compose.yml -f compose.prod.yml up --build -d

View file

@ -11,6 +11,8 @@ on:
pull_request: pull_request:
branches: branches:
- dev - dev
types: ["synchronize", "ready_for_review", "opened", "reopened"]
# Down scope as necessary via https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token # Down scope as necessary via https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
permissions: permissions:
@ -20,6 +22,7 @@ permissions:
jobs: jobs:
run-linters: run-linters:
name: Run linters name: Run linters
if: '! github.event.pull_request.draft'
runs-on: [self-hosted, Linux, X64] runs-on: [self-hosted, Linux, X64]
steps: steps:
@ -42,4 +45,4 @@ jobs:
eslint: true eslint: true
eslint_args: '--config eslint.config.ts' eslint_args: '--config eslint.config.ts'
prettier: true prettier: true
commit_message: 'style: fix linting issues met ${linter}' commit_message: 'style: fix linting issues met ${linter}'

View file

@ -19,7 +19,7 @@ export class Submission {
learningObjectVersion: number = 1; learningObjectVersion: number = 1;
@PrimaryKey({ type: 'integer', autoincrement: true }) @PrimaryKey({ type: 'integer', autoincrement: true })
submissionNumber!: number; submissionNumber?: number;
@ManyToOne({ @ManyToOne({
entity: () => Student, entity: () => Student,

View file

@ -39,14 +39,18 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
* Convert the given learning path entity to an object which conforms to the learning path content. * Convert the given learning path entity to an object which conforms to the learning path content.
*/ */
async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: PersonalizationTarget): Promise<LearningPath> { async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: PersonalizationTarget): Promise<LearningPath> {
// Fetch the corresponding learning object for each node since some parts of the expected response contains parts
// With information which is not available in the LearningPathNodes themselves.
const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes); const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes);
const targetAges = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.targetAges || []); // The target ages of a learning path are the union of the target ages of all learning objects.
const targetAges = [...new Set(Array.from(nodesToLearningObjects.values()).flatMap((it) => it.targetAges || []))];
const keywords = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.keywords || []); // The keywords of the learning path consist of the union of the keywords of all learning objects.
const keywords = [...new Set(Array.from(nodesToLearningObjects.values()).flatMap((it) => it.keywords || []))];
const image = learningPath.image ? learningPath.image.toString('base64') : undefined; const image = learningPath.image ? learningPath.image.toString('base64') : undefined;
// Convert the learning object notes as retrieved from the database into the expected response format-
const convertedNodes = await convertNodes(nodesToLearningObjects, personalizedFor); const convertedNodes = await convertNodes(nodesToLearningObjects, personalizedFor);
return { return {
@ -67,34 +71,55 @@ async function convertLearningPath(learningPath: LearningPathEntity, order: numb
}; };
} }
/**
* Helper function converting a single learning path node (as represented in the database) and the corresponding
* learning object into a learning path node as it should be represented in the API.
*
* @param node Learning path node as represented in the database.
* @param learningObject Learning object the learning path node refers to.
* @param personalizedFor Personalization target if a personalized learning path is desired.
* @param nodesToLearningObjects Mapping from learning path nodes to the corresponding learning objects.
*/
async function convertNode(
node: LearningPathNode,
learningObject: FilteredLearningObject,
personalizedFor: PersonalizationTarget | undefined,
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>
): Promise<LearningObjectNode> {
const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null;
const transitions = node.transitions
.filter(
(trans) =>
!personalizedFor || // If we do not want a personalized learning path, keep all transitions
isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // Otherwise remove all transitions that aren't possible.
)
.map((trans, i) => convertTransition(trans, i, nodesToLearningObjects));
return {
_id: learningObject.uuid,
language: learningObject.language,
start_node: node.startNode,
created_at: node.createdAt.toISOString(),
updatedAt: node.updatedAt.toISOString(),
learningobject_hruid: node.learningObjectHruid,
version: learningObject.version,
transitions,
};
}
/** /**
* Helper function converting pairs of learning path nodes (as represented in the database) and the corresponding * Helper function converting pairs of learning path nodes (as represented in the database) and the corresponding
* learning objects into a list of learning path nodes as they should be represented in the API. * learning objects into a list of learning path nodes as they should be represented in the API.
* @param nodesToLearningObjects *
* @param personalizedFor * @param nodesToLearningObjects Mapping from learning path nodes to the corresponding learning objects.
* @param personalizedFor Personalization target if a personalized learning path is desired.
*/ */
async function convertNodes( async function convertNodes(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>, nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget personalizedFor?: PersonalizationTarget
): Promise<LearningObjectNode[]> { ): Promise<LearningObjectNode[]> {
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => { const nodesPromise = Array.from(nodesToLearningObjects.entries()).map((entry) =>
const [node, learningObject] = entry; convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects)
const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null; );
return {
_id: learningObject.uuid,
language: learningObject.language,
start_node: node.startNode,
created_at: node.createdAt.toISOString(),
updatedAt: node.updatedAt.toISOString(),
learningobject_hruid: node.learningObjectHruid,
version: learningObject.version,
transitions: node.transitions
.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
};
});
return await Promise.all(nodesPromise); return await Promise.all(nodesPromise);
} }
@ -112,9 +137,10 @@ function optionalJsonStringToObject(jsonString?: string): object | null {
* Helper function which converts a transition in the database representation to a transition in the representation * Helper function which converts a transition in the database representation to a transition in the representation
* the Dwengo API uses. * the Dwengo API uses.
* *
* @param transition * @param transition The transition to convert
* @param index * @param index The sequence number of the transition to convert
* @param nodesToLearningObjects * @param nodesToLearningObjects Map which maps each learning path node of the current learning path to the learning
* object it refers to.
*/ */
function convertTransition( function convertTransition(
transition: LearningPathTransition, transition: LearningPathTransition,

View file

@ -12,7 +12,6 @@ import learningObjectExample from '../../test-assets/learning-objects/pn-werking
import learningPathExample from '../../test-assets/learning-paths/pn-werking-example.js'; import learningPathExample from '../../test-assets/learning-paths/pn-werking-example.js';
import databaseLearningPathProvider from '../../../src/services/learning-paths/database-learning-path-provider.js'; import databaseLearningPathProvider from '../../../src/services/learning-paths/database-learning-path-provider.js';
import { expectToBeCorrectLearningPath } from '../../test-utils/expectations.js'; import { expectToBeCorrectLearningPath } from '../../test-utils/expectations.js';
import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js';
import learningObjectService from '../../../src/services/learning-objects/learning-object-service.js'; import learningObjectService from '../../../src/services/learning-objects/learning-object-service.js';
import { Language } from '../../../src/entities/content/language.js'; import { Language } from '../../../src/entities/content/language.js';
import { import {
@ -59,7 +58,6 @@ async function initPersonalizationTestData(): Promise<{
learningObjectHruid: learningContent.branchingObject.hruid, learningObjectHruid: learningContent.branchingObject.hruid,
learningObjectLanguage: learningContent.branchingObject.language, learningObjectLanguage: learningContent.branchingObject.language,
learningObjectVersion: learningContent.branchingObject.version, learningObjectVersion: learningContent.branchingObject.version,
submissionNumber: 0,
submitter: studentA, submitter: studentA,
submissionTime: new Date(), submissionTime: new Date(),
content: '[0]', content: '[0]',
@ -76,7 +74,6 @@ async function initPersonalizationTestData(): Promise<{
learningObjectHruid: learningContent.branchingObject.hruid, learningObjectHruid: learningContent.branchingObject.hruid,
learningObjectLanguage: learningContent.branchingObject.language, learningObjectLanguage: learningContent.branchingObject.language,
learningObjectVersion: learningContent.branchingObject.version, learningObjectVersion: learningContent.branchingObject.version,
submissionNumber: 1,
submitter: studentB, submitter: studentB,
submissionTime: new Date(), submissionTime: new Date(),
content: '[1]', content: '[1]',
@ -106,7 +103,6 @@ function expectBranchingObjectNode(
} }
describe('DatabaseLearningPathProvider', () => { describe('DatabaseLearningPathProvider', () => {
let learningObjectRepo: LearningObjectRepository;
let example: { learningObject: LearningObject; learningPath: LearningPath }; let example: { learningObject: LearningObject; learningPath: LearningPath };
let persTestData: { learningContent: ConditionTestLearningPathAndLearningObjects; studentA: Student; studentB: Student }; let persTestData: { learningContent: ConditionTestLearningPathAndLearningObjects; studentA: Student; studentB: Student };
@ -114,7 +110,6 @@ describe('DatabaseLearningPathProvider', () => {
await setupTestApp(); await setupTestApp();
example = await initExampleData(); example = await initExampleData();
persTestData = await initPersonalizationTestData(); persTestData = await initPersonalizationTestData();
learningObjectRepo = getLearningObjectRepository();
}); });
describe('fetchLearningPaths', () => { describe('fetchLearningPaths', () => {

2640
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -31,6 +31,7 @@
"@types/eslint-config-prettier": "^6.11.3", "@types/eslint-config-prettier": "^6.11.3",
"@typescript-eslint/eslint-plugin": "^8.24.1", "@typescript-eslint/eslint-plugin": "^8.24.1",
"@typescript-eslint/parser": "^8.24.1", "@typescript-eslint/parser": "^8.24.1",
"@vitest/coverage-v8": "^3.0.8",
"eslint": "^9.20.1", "eslint": "^9.20.1",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"jiti": "^2.4.2", "jiti": "^2.4.2",