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 | ||||||
|  |          | ||||||
							
								
								
									
										3
									
								
								.github/workflows/lint-action.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.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: | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  | @ -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 |  * Helper function converting a single learning path node (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 object into a learning path node as it should be represented in the API. | ||||||
|  * @param nodesToLearningObjects |  * | ||||||
|  * @param personalizedFor |  * @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( | async function convertNode( | ||||||
|     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>, |     node: LearningPathNode, | ||||||
|     personalizedFor?: PersonalizationTarget |     learningObject: FilteredLearningObject, | ||||||
| ): Promise<LearningObjectNode[]> { |     personalizedFor: PersonalizationTarget | undefined, | ||||||
|     const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => { |     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> | ||||||
|         const [node, learningObject] = entry; | ): Promise<LearningObjectNode> { | ||||||
|     const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null; |     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 { |     return { | ||||||
|         _id: learningObject.uuid, |         _id: learningObject.uuid, | ||||||
|         language: learningObject.language, |         language: learningObject.language, | ||||||
|  | @ -88,13 +102,24 @@ async function convertNodes( | ||||||
|         updatedAt: node.updatedAt.toISOString(), |         updatedAt: node.updatedAt.toISOString(), | ||||||
|         learningobject_hruid: node.learningObjectHruid, |         learningobject_hruid: node.learningObjectHruid, | ||||||
|         version: learningObject.version, |         version: learningObject.version, | ||||||
|             transitions: node.transitions |         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
 |  | ||||||
|     }; |     }; | ||||||
|     }); | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 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); |     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", | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl