Merge branch 'dev' into chore/development-workflow

This commit is contained in:
Tibo De Peuter 2025-03-25 10:06:54 +01:00
commit 8389414ea4
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
31 changed files with 1907 additions and 124 deletions

View file

@ -28,9 +28,9 @@ curricula_page:
contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai:
title: Basisprincipes van AI
sub_title: Basisprincipes van AI
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.'
title: Grundlagen der KI
sub_title: Grundlagen der KI
description: 'Dieses Thema bündelt verschiedene Aktivitäten, in denen die grundlegenden Prinzipien der künstlichen Intelligenz (KI) behandelt werden. Die Schüler lernen, was KI ist, wie sie funktioniert und wie sie in verschiedenen Bereichen angewendet werden kann.'
contact: ''
kiks:
title: KI und Klima

View file

@ -28,10 +28,11 @@ curricula_page:
contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai:
title: Basisprincipes van AI
sub_title: Basisprincipes van AI
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.'
title: Basics of AI
sub_title: Basics of AI
description: 'This theme brings together various activities covering the basic principles of Artificial Intelligence (AI). Students learn what AI is, how it works, and how it can be applied in different domains.'
contact: ''
kiks:
title: AI and Climate
sub_title: KIKS

View file

@ -28,9 +28,9 @@ curricula_page:
contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai:
title: Basisprincipes van AI
sub_title: Basisprincipes van AI
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.'
title: Principes de base de lIA
sub_title: Principes de base de lIA
description: 'Ce thème rassemble différentes activités portant sur les principes fondamentaux de lintelligence artificielle (IA). Les élèves apprennent ce quest lIA, comment elle fonctionne et comment elle peut être appliquée dans divers domaines.'
contact: ''
kiks:
title: 'IA et changement climatique'

View file

@ -8,7 +8,7 @@ interface Translations {
};
}
export function getThemes(req: Request, res: Response) {
export function getThemesHandler(req: Request, res: Response) {
const language = (req.query.language as string)?.toLowerCase() || 'nl';
const translations = loadTranslations<Translations>(language);
const themeList = themes.map((theme) => ({
@ -21,8 +21,14 @@ export function getThemes(req: Request, res: Response) {
res.json(themeList);
}
export function getThemeByTitle(req: Request, res: Response) {
export function getHruidsByThemeHandler(req: Request, res: Response) {
const themeKey = req.params.theme;
if (!themeKey) {
res.status(400).json({ error: 'Missing required field: theme' });
return;
}
const theme = themes.find((t) => t.title === themeKey);
if (theme) {

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

@ -1,14 +1,14 @@
import express from 'express';
import { getThemes, getThemeByTitle } from '../controllers/themes.js';
import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js';
const router = express.Router();
// Query: language
// Route to fetch list of {key, title, description, image} themes in their respective language
router.get('/', getThemes);
router.get('/', getThemesHandler);
// Arg: theme (key)
// Route to fetch list of hruids based on theme
router.get('/:theme', getThemeByTitle);
router.get('/:theme', getHruidsByThemeHandler);
export default router;

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 {
@ -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
* 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(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget
): Promise<LearningObjectNode[]> {
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => {
const [node, learningObject] = entry;
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
};
});
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', () => {