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:
branches:
- 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
permissions:
@ -20,6 +22,7 @@ permissions:
jobs:
run-linters:
name: Run linters
if: '! github.event.pull_request.draft'
runs-on: [self-hosted, Linux, X64]
steps:

View file

@ -19,7 +19,7 @@ export class Submission {
learningObjectVersion: number = 1;
@PrimaryKey({ type: 'integer', autoincrement: true })
submissionNumber!: number;
submissionNumber?: number;
@ManyToOne({
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.
*/
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 targetAges = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.targetAges || []);
const keywords = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.keywords || []);
// 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 || []))];
// 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;
// Convert the learning object notes as retrieved from the database into the expected response format-
const convertedNodes = await convertNodes(nodesToLearningObjects, personalizedFor);
return {
@ -68,18 +72,28 @@ async function convertLearningPath(learningPath: LearningPathEntity, order: numb
}
/**
* 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.
* @param nodesToLearningObjects
* @param personalizedFor
* 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 convertNodes(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget
): Promise<LearningObjectNode[]> {
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => {
const [node, learningObject] = entry;
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,
@ -88,13 +102,24 @@ async function convertNodes(
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
transitions,
};
});
}
/**
* 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.
*
* @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(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget
): Promise<LearningObjectNode[]> {
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map((entry) =>
convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects)
);
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
* the Dwengo API uses.
*
* @param transition
* @param index
* @param nodesToLearningObjects
* @param transition The transition to convert
* @param index The sequence number of the transition to convert
* @param nodesToLearningObjects Map which maps each learning path node of the current learning path to the learning
* object it refers to.
*/
function convertTransition(
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 databaseLearningPathProvider from '../../../src/services/learning-paths/database-learning-path-provider.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 { Language } from '../../../src/entities/content/language.js';
import {
@ -59,7 +58,6 @@ async function initPersonalizationTestData(): Promise<{
learningObjectHruid: learningContent.branchingObject.hruid,
learningObjectLanguage: learningContent.branchingObject.language,
learningObjectVersion: learningContent.branchingObject.version,
submissionNumber: 0,
submitter: studentA,
submissionTime: new Date(),
content: '[0]',
@ -76,7 +74,6 @@ async function initPersonalizationTestData(): Promise<{
learningObjectHruid: learningContent.branchingObject.hruid,
learningObjectLanguage: learningContent.branchingObject.language,
learningObjectVersion: learningContent.branchingObject.version,
submissionNumber: 1,
submitter: studentB,
submissionTime: new Date(),
content: '[1]',
@ -106,7 +103,6 @@ function expectBranchingObjectNode(
}
describe('DatabaseLearningPathProvider', () => {
let learningObjectRepo: LearningObjectRepository;
let example: { learningObject: LearningObject; learningPath: LearningPath };
let persTestData: { learningContent: ConditionTestLearningPathAndLearningObjects; studentA: Student; studentB: Student };
@ -114,7 +110,6 @@ describe('DatabaseLearningPathProvider', () => {
await setupTestApp();
example = await initExampleData();
persTestData = await initPersonalizationTestData();
learningObjectRepo = getLearningObjectRepository();
});
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",
"@typescript-eslint/eslint-plugin": "^8.24.1",
"@typescript-eslint/parser": "^8.24.1",
"@vitest/coverage-v8": "^3.0.8",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.0.1",
"jiti": "^2.4.2",