Merge remote-tracking branch 'origin/dev' into feat/user-homepage
# Conflicts: # package-lock.json
This commit is contained in:
commit
4698311062
7 changed files with 674 additions and 2076 deletions
19
.github/workflows/deployment.yml
vendored
Normal file
19
.github/workflows/deployment.yml
vendored
Normal 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
|
||||||
|
|
5
.github/workflows/lint-action.yml
vendored
5
.github/workflows/lint-action.yml
vendored
|
@ -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}'
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
2640
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue