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

@ -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,