From c1c10e27c1cb458508e30012a7d850bf9613260b Mon Sep 17 00:00:00 2001 From: Joyelle Ndagijimana Date: Sun, 2 Mar 2025 19:46:12 +0100 Subject: [PATCH 001/119] feat(frontend): i18n en vertalingsbestanden initialisatie --- frontend/src/i18n/i18n.ts | 21 +++++++++++++++++++++ frontend/src/i18n/locales/de.json | 0 frontend/src/i18n/locales/en.json | 0 frontend/src/i18n/locales/fr.json | 0 frontend/src/i18n/locales/nl.json | 0 frontend/src/main.ts | 8 +++++--- tsconfig.json | 3 ++- 7 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 frontend/src/i18n/i18n.ts create mode 100644 frontend/src/i18n/locales/de.json create mode 100644 frontend/src/i18n/locales/en.json create mode 100644 frontend/src/i18n/locales/fr.json create mode 100644 frontend/src/i18n/locales/nl.json diff --git a/frontend/src/i18n/i18n.ts b/frontend/src/i18n/i18n.ts new file mode 100644 index 00000000..a695a15d --- /dev/null +++ b/frontend/src/i18n/i18n.ts @@ -0,0 +1,21 @@ +import { createI18n } from "vue-i18n"; + +// Import translations +import en from "@/i18n/locales/en.json"; +import nl from "@/i18n/locales/nl.json"; +import fr from "@/i18n/locales/fr.json"; +import de from "@/i18n/locales/de.json"; + +const i18n = createI18n({ + //legacy: false, + locale: "en", + fallbackLocale: "en", + messages: { + en: { ...en }, + nl: { ...nl }, + fr: { ...fr }, + de: { ...de }, + }, +}); + +export default i18n; diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/i18n/locales/nl.json b/frontend/src/i18n/locales/nl.json new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 7f6db814..e82313b5 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,15 +1,17 @@ -import { createApp } from "vue"; +import {createApp} from "vue"; // Vuetify import "vuetify/styles"; -import { createVuetify } from "vuetify"; +import {createVuetify} from "vuetify"; import * as components from "vuetify/components"; import * as directives from "vuetify/directives"; +import i18n from "./i18n/i18n.ts"; // Components import App from "./App.vue"; import router from "./router"; + const app = createApp(App); app.use(router); @@ -24,5 +26,5 @@ const vuetify = createVuetify({ directives, }); app.use(vuetify); - +app.use(i18n); app.mount("#app"); diff --git a/tsconfig.json b/tsconfig.json index b63e52cc..b41449cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -118,7 +118,8 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "resolveJsonModule": true } } From d7728ddd0341a9a6afeec3fbe237cdd8e7f51576 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 3 Mar 2025 22:19:09 +0100 Subject: [PATCH 002/119] refactor(backend): Preparation for learning content from multiple data sources. Refactored the service layer so that it becomes possible to add another source for learning objects and learning paths. --- backend/src/controllers/learningObjects.ts | 4 +- backend/src/controllers/learningPaths.ts | 2 +- .../{learningPath.ts => learningContent.ts} | 4 +- .../dwengo-api-learning-object-provider.ts} | 113 ++++++++++-------- .../dwengo-api-learning-path-provider.ts | 65 ++++++++++ .../learning-object-provider.ts | 18 +++ .../learning-object-service.ts | 30 +++++ .../learning-path-provider.ts | 16 +++ .../learning-content/learning-path-service.ts | 23 ++++ backend/src/services/learningPaths.ts | 61 ---------- 10 files changed, 219 insertions(+), 117 deletions(-) rename backend/src/interfaces/{learningPath.ts => learningContent.ts} (96%) rename backend/src/services/{learningObjects.ts => learning-content/dwengo-api/dwengo-api-learning-object-provider.ts} (52%) create mode 100644 backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts create mode 100644 backend/src/services/learning-content/learning-object-provider.ts create mode 100644 backend/src/services/learning-content/learning-object-service.ts create mode 100644 backend/src/services/learning-content/learning-path-provider.ts create mode 100644 backend/src/services/learning-content/learning-path-service.ts delete mode 100644 backend/src/services/learningPaths.ts diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts index 4295326a..e0a7cd4a 100644 --- a/backend/src/controllers/learningObjects.ts +++ b/backend/src/controllers/learningObjects.ts @@ -3,9 +3,9 @@ import { getLearningObjectById, getLearningObjectIdsFromPath, getLearningObjectsFromPath, -} from '../services/learningObjects.js'; +} from '../services/learning-content/dwengo-api/dwengo-api-learning-object-provider.js'; import { FALLBACK_LANG } from '../config.js'; -import { FilteredLearningObject } from '../interfaces/learningPath'; +import { FilteredLearningObject } from '../interfaces/learningContent'; export async function getAllLearningObjects( req: Request, diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts index 903451be..f05532d4 100644 --- a/backend/src/controllers/learningPaths.ts +++ b/backend/src/controllers/learningPaths.ts @@ -4,7 +4,7 @@ import { FALLBACK_LANG } from '../config.js'; import { fetchLearningPaths, searchLearningPaths, -} from '../services/learningPaths.js'; +} from '../services/learning-content/dwengo-api/dwengo-api-learning-path-provider.js'; /** * Fetch learning paths based on query parameters. */ diff --git a/backend/src/interfaces/learningPath.ts b/backend/src/interfaces/learningContent.ts similarity index 96% rename from backend/src/interfaces/learningPath.ts rename to backend/src/interfaces/learningContent.ts index 1e2cc6ef..9811543e 100644 --- a/backend/src/interfaces/learningPath.ts +++ b/backend/src/interfaces/learningContent.ts @@ -20,7 +20,7 @@ export interface LearningObjectNode { updatedAt: string; } -export interface LearningPath { +export interface LearningContent { _id: string; language: string; hruid: string; @@ -93,6 +93,6 @@ export interface FilteredLearningObject { export interface LearningPathResponse { success: boolean; source: string; - data: LearningPath[] | null; + data: LearningContent[] | null; message?: string; } diff --git a/backend/src/services/learningObjects.ts b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts similarity index 52% rename from backend/src/services/learningObjects.ts rename to backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts index d1d34ad2..c5e52695 100644 --- a/backend/src/services/learningObjects.ts +++ b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts @@ -1,13 +1,20 @@ -import { DWENGO_API_BASE } from '../config.js'; -import { fetchWithLogging } from '../util/apiHelper.js'; +import { DWENGO_API_BASE } from '../../../config.js'; +import { fetchWithLogging } from '../../../util/apiHelper.js'; import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse, -} from '../interfaces/learningPath.js'; -import { fetchLearningPaths } from './learningPaths.js'; +} from '../../../interfaces/learningContent.js'; +import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; +import {LearningObjectProvider} from "../learning-object-provider"; +/** + * Helper function to convert the learning object metadata retrieved from the API to a FilteredLearningObject which + * our API should return. + * @param data + * @param htmlUrl + */ function filterData( data: LearningObjectMetadata, htmlUrl: string @@ -36,29 +43,7 @@ function filterData( } /** - * Fetches a single learning object by its HRUID - */ -export async function getLearningObjectById( - hruid: string, - language: string -): Promise { - const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; - const metadata = await fetchWithLogging( - metadataUrl, - `Metadata for Learning Object HRUID "${hruid}" (language ${language})` - ); - - if (!metadata) { - console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); - return null; - } - - const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`; - return filterData(metadata, htmlUrl); -} - -/** - * Generic function to fetch learning objects (full data or just HRUIDs) + * Generic helper function to fetch learning objects (full data or just HRUIDs) */ async function fetchLearningObjects( hruid: string, @@ -67,7 +52,7 @@ async function fetchLearningObjects( ): Promise { try { const learningPathResponse: LearningPathResponse = - await fetchLearningPaths( + await dwengoApiLearningPathProvider.fetchLearningPaths( [hruid], language, `Learning path for HRUID "${hruid}"` @@ -93,7 +78,7 @@ async function fetchLearningObjects( return await Promise.all( nodes.map(async (node) => { - return getLearningObjectById( + return dwengoApiLearningObjectProvider.getLearningObjectById( node.learningobject_hruid, language ); @@ -109,26 +94,52 @@ async function fetchLearningObjects( } } -/** - * Fetch full learning object data (metadata) - */ -export async function getLearningObjectsFromPath( - hruid: string, - language: string -): Promise { - return (await fetchLearningObjects( - hruid, - true, - language - )) as FilteredLearningObject[]; -} +const dwengoApiLearningObjectProvider: LearningObjectProvider = { + /** + * Fetches a single learning object by its HRUID + */ + async getLearningObjectById( + hruid: string, + language: string + ): Promise { + const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; + const metadata = await fetchWithLogging( + metadataUrl, + `Metadata for Learning Object HRUID "${hruid}" (language ${language})` + ); -/** - * Fetch only learning object HRUIDs - */ -export async function getLearningObjectIdsFromPath( - hruid: string, - language: string -): Promise { - return (await fetchLearningObjects(hruid, false, language)) as string[]; -} + if (!metadata) { + console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); + return null; + } + + const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`; + return filterData(metadata, htmlUrl); + }, + + /** + * Fetch full learning object data (metadata) + */ + async getLearningObjectsFromPath( + hruid: string, + language: string + ): Promise { + return (await fetchLearningObjects( + hruid, + true, + language + )) as FilteredLearningObject[]; + }, + + /** + * Fetch only learning object HRUIDs + */ + async getLearningObjectIdsFromPath( + hruid: string, + language: string + ): Promise { + return (await fetchLearningObjects(hruid, false, language)) as string[]; + } +}; + +export default dwengoApiLearningObjectProvider; diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts new file mode 100644 index 00000000..23662646 --- /dev/null +++ b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts @@ -0,0 +1,65 @@ +import { fetchWithLogging } from '../../../util/apiHelper.js'; +import { DWENGO_API_BASE } from '../../../config.js'; +import { + LearningContent, + LearningPathResponse, +} from '../../../interfaces/learningContent.js'; +import {LearningPathProvider} from "../learning-path-provider"; + +const dwengoApiLearningPathProvider: LearningPathProvider = { + async fetchLearningPaths( + hruids: string[], + language: string, + source: string + ): Promise { + if (hruids.length === 0) { + return { + success: false, + source, + data: null, + message: `No HRUIDs provided for ${source}.`, + }; + } + + const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; + const params = { pathIdList: JSON.stringify({ hruids }), language }; + + const learningPaths = await fetchWithLogging( + apiUrl, + `Learning paths for ${source}`, + params + ); + + if (!learningPaths || learningPaths.length === 0) { + console.error(`⚠️ WARNING: No learning paths found for ${source}.`); + return { + success: false, + source, + data: [], + message: `No learning paths found for ${source}.`, + }; + } + + return { + success: true, + source, + data: learningPaths, + }; + }, + async searchLearningPaths( + query: string, + language: string + ): Promise { + const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; + const params = { all: query, language }; + + const searchResults = await fetchWithLogging( + apiUrl, + `Search learning paths with query "${query}"`, + params + ); + return searchResults ?? []; + } +}; + +export default dwengoApiLearningPathProvider; diff --git a/backend/src/services/learning-content/learning-object-provider.ts b/backend/src/services/learning-content/learning-object-provider.ts new file mode 100644 index 00000000..e37c4442 --- /dev/null +++ b/backend/src/services/learning-content/learning-object-provider.ts @@ -0,0 +1,18 @@ +import {FilteredLearningObject} from "../../interfaces/learningContent"; + +export interface LearningObjectProvider { + /** + * Fetches a single learning object by its HRUID + */ + getLearningObjectById(hruid: string, language: string): Promise; + + /** + * Fetch full learning object data (metadata) + */ + getLearningObjectsFromPath(hruid: string, language: string): Promise; + + /** + * Fetch only learning object HRUIDs + */ + getLearningObjectIdsFromPath(hruid: string, language: string): Promise; +} diff --git a/backend/src/services/learning-content/learning-object-service.ts b/backend/src/services/learning-content/learning-object-service.ts new file mode 100644 index 00000000..e0beec60 --- /dev/null +++ b/backend/src/services/learning-content/learning-object-service.ts @@ -0,0 +1,30 @@ +import {FilteredLearningObject} from "../../interfaces/learningContent"; +import dwengoApiLearningObjectProvider from "./dwengo-api/dwengo-api-learning-object-provider"; + +/** + * Service providing access to data about learning objects from the appropriate data source (database or Dwengo-api) + */ +const learningObjectService = { + /** + * Fetches a single learning object by its HRUID + */ + getLearningObjectById(hruid: string, language: string): Promise { + return dwengoApiLearningObjectProvider.getLearningObjectById(hruid, language); + }, + + /** + * Fetch full learning object data (metadata) + */ + getLearningObjectsFromPath(hruid: string, language: string): Promise { + return dwengoApiLearningObjectProvider.getLearningObjectsFromPath(hruid, language); + }, + + /** + * Fetch only learning object HRUIDs + */ + getLearningObjectIdsFromPath(hruid: string, language: string): Promise { + return dwengoApiLearningObjectProvider.getLearningObjectIdsFromPath(hruid, language); + } +}; + +export default learningObjectService; diff --git a/backend/src/services/learning-content/learning-path-provider.ts b/backend/src/services/learning-content/learning-path-provider.ts new file mode 100644 index 00000000..f236cd82 --- /dev/null +++ b/backend/src/services/learning-content/learning-path-provider.ts @@ -0,0 +1,16 @@ +import {LearningContent, LearningPathResponse} from "../../interfaces/learningContent"; + +/** + * Generic interface for a service which provides access to learning paths from a data source. + */ +export interface LearningPathProvider { + /** + * Fetch the learning paths with the given hruids from the data source. + */ + fetchLearningPaths(hruids: string[], language: string, source: string): Promise; + + /** + * Search learning paths in the data source using the given search string. + */ + searchLearningPaths(query: string, language: string): Promise; +} diff --git a/backend/src/services/learning-content/learning-path-service.ts b/backend/src/services/learning-content/learning-path-service.ts new file mode 100644 index 00000000..31e10558 --- /dev/null +++ b/backend/src/services/learning-content/learning-path-service.ts @@ -0,0 +1,23 @@ +import {LearningContent, LearningPathResponse} from "../../interfaces/learningContent"; +import dwengoApiLearningPathProvider from "./dwengo-api/dwengo-api-learning-path-provider"; + +/** + * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) + */ +const learningPathService = { + /** + * Fetch the learning paths with the given hruids from the data source. + */ + fetchLearningPaths(hruids: string[], language: string, source: string): Promise { + return dwengoApiLearningPathProvider.fetchLearningPaths(hruids, language, source); + }, + + /** + * Search learning paths in the data source using the given search string. + */ + searchLearningPaths(query: string, language: string): Promise { + return dwengoApiLearningPathProvider.searchLearningPaths(query, language); + } +} + +export default learningPathService; diff --git a/backend/src/services/learningPaths.ts b/backend/src/services/learningPaths.ts deleted file mode 100644 index 2a9f15a3..00000000 --- a/backend/src/services/learningPaths.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { fetchWithLogging } from '../util/apiHelper.js'; -import { DWENGO_API_BASE } from '../config.js'; -import { - LearningPath, - LearningPathResponse, -} from '../interfaces/learningPath.js'; - -export async function fetchLearningPaths( - hruids: string[], - language: string, - source: string -): Promise { - if (hruids.length === 0) { - return { - success: false, - source, - data: null, - message: `No HRUIDs provided for ${source}.`, - }; - } - - const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; - const params = { pathIdList: JSON.stringify({ hruids }), language }; - - const learningPaths = await fetchWithLogging( - apiUrl, - `Learning paths for ${source}`, - params - ); - - if (!learningPaths || learningPaths.length === 0) { - console.error(`⚠️ WARNING: No learning paths found for ${source}.`); - return { - success: false, - source, - data: [], - message: `No learning paths found for ${source}.`, - }; - } - - return { - success: true, - source, - data: learningPaths, - }; -} - -export async function searchLearningPaths( - query: string, - language: string -): Promise { - const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; - const params = { all: query, language }; - - const searchResults = await fetchWithLogging( - apiUrl, - `Search learning paths with query "${query}"`, - params - ); - return searchResults ?? []; -} From e660c185b53d1cb9883c6f089bc632fb6d739b2f Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 3 Mar 2025 22:21:47 +0100 Subject: [PATCH 003/119] fix(backend): Forgot to adjust some files. --- backend/src/controllers/learningObjects.ts | 12 ++++-------- backend/src/controllers/learningPaths.ts | 9 +++------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts index e0a7cd4a..2455b49c 100644 --- a/backend/src/controllers/learningObjects.ts +++ b/backend/src/controllers/learningObjects.ts @@ -1,11 +1,7 @@ import { Request, Response } from 'express'; -import { - getLearningObjectById, - getLearningObjectIdsFromPath, - getLearningObjectsFromPath, -} from '../services/learning-content/dwengo-api/dwengo-api-learning-object-provider.js'; import { FALLBACK_LANG } from '../config.js'; import { FilteredLearningObject } from '../interfaces/learningContent'; +import learningObjectService from "../services/learning-content/learning-object-service"; export async function getAllLearningObjects( req: Request, @@ -23,9 +19,9 @@ export async function getAllLearningObjects( let learningObjects: FilteredLearningObject[] | string[]; if (full) { - learningObjects = await getLearningObjectsFromPath(hruid, language); + learningObjects = await learningObjectService.getLearningObjectsFromPath(hruid, language); } else { - learningObjects = await getLearningObjectIdsFromPath( + learningObjects = await learningObjectService.getLearningObjectIdsFromPath( hruid, language ); @@ -51,7 +47,7 @@ export async function getLearningObject( return; } - const learningObject = await getLearningObjectById(hruid, language); + const learningObject = await learningObjectService.getLearningObjectById(hruid, language); res.json(learningObject); } catch (error) { console.error('Error fetching learning object:', error); diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts index f05532d4..af521157 100644 --- a/backend/src/controllers/learningPaths.ts +++ b/backend/src/controllers/learningPaths.ts @@ -1,10 +1,7 @@ import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; import { FALLBACK_LANG } from '../config.js'; -import { - fetchLearningPaths, - searchLearningPaths, -} from '../services/learning-content/dwengo-api/dwengo-api-learning-path-provider.js'; +import learningPathService from "../services/learning-content/learning-path-service"; /** * Fetch learning paths based on query parameters. */ @@ -37,7 +34,7 @@ export async function getLearningPaths( return; } } else if (searchQuery) { - const searchResults = await searchLearningPaths( + const searchResults = await learningPathService.searchLearningPaths( searchQuery, language ); @@ -49,7 +46,7 @@ export async function getLearningPaths( }); } - const learningPaths = await fetchLearningPaths( + const learningPaths = await learningPathService.fetchLearningPaths( hruidList, language, `HRUIDs: ${hruidList.join(', ')}` From 770c5c987931a19a04e61a7216ed89700d56161c Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 3 Mar 2025 22:25:00 +0100 Subject: [PATCH 004/119] fix(backend): Undid accidental rename of LearningPath to LearningContent. --- .../dwengo-api/dwengo-api-learning-path-provider.ts | 10 +++++----- .../learning-content/learning-path-provider.ts | 4 ++-- .../services/learning-content/learning-path-service.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts index 23662646..f7b4ff7d 100644 --- a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts @@ -1,9 +1,9 @@ import { fetchWithLogging } from '../../../util/apiHelper.js'; import { DWENGO_API_BASE } from '../../../config.js'; import { - LearningContent, + LearningPath, LearningPathResponse, -} from '../../../interfaces/learningContent.js'; +} from '../../../interfaces/learning-content.js'; import {LearningPathProvider} from "../learning-path-provider"; const dwengoApiLearningPathProvider: LearningPathProvider = { @@ -24,7 +24,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; const params = { pathIdList: JSON.stringify({ hruids }), language }; - const learningPaths = await fetchWithLogging( + const learningPaths = await fetchWithLogging( apiUrl, `Learning paths for ${source}`, params @@ -49,11 +49,11 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { async searchLearningPaths( query: string, language: string - ): Promise { + ): Promise { const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; const params = { all: query, language }; - const searchResults = await fetchWithLogging( + const searchResults = await fetchWithLogging( apiUrl, `Search learning paths with query "${query}"`, params diff --git a/backend/src/services/learning-content/learning-path-provider.ts b/backend/src/services/learning-content/learning-path-provider.ts index f236cd82..e9948963 100644 --- a/backend/src/services/learning-content/learning-path-provider.ts +++ b/backend/src/services/learning-content/learning-path-provider.ts @@ -1,4 +1,4 @@ -import {LearningContent, LearningPathResponse} from "../../interfaces/learningContent"; +import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; /** * Generic interface for a service which provides access to learning paths from a data source. @@ -12,5 +12,5 @@ export interface LearningPathProvider { /** * Search learning paths in the data source using the given search string. */ - searchLearningPaths(query: string, language: string): Promise; + searchLearningPaths(query: string, language: string): Promise; } diff --git a/backend/src/services/learning-content/learning-path-service.ts b/backend/src/services/learning-content/learning-path-service.ts index 31e10558..0bb925bf 100644 --- a/backend/src/services/learning-content/learning-path-service.ts +++ b/backend/src/services/learning-content/learning-path-service.ts @@ -1,4 +1,4 @@ -import {LearningContent, LearningPathResponse} from "../../interfaces/learningContent"; +import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; import dwengoApiLearningPathProvider from "./dwengo-api/dwengo-api-learning-path-provider"; /** @@ -15,7 +15,7 @@ const learningPathService = { /** * Search learning paths in the data source using the given search string. */ - searchLearningPaths(query: string, language: string): Promise { + searchLearningPaths(query: string, language: string): Promise { return dwengoApiLearningPathProvider.searchLearningPaths(query, language); } } From 18ee991ce3cb6d3589d8c69f69d4acba61a98b03 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 4 Mar 2025 22:35:05 +0100 Subject: [PATCH 005/119] feat(backend): Added endpoint to fetch HTML version of learning object (from Dwengo backend) Also refactored a bit to make this easier. --- backend/src/app.ts | 4 +- backend/src/config.ts | 6 +- backend/src/controllers/learning-objects.ts | 62 +++++++++++++ backend/src/controllers/learning-paths.ts | 53 +++++++++++ backend/src/controllers/learningObjects.ts | 56 ------------ backend/src/controllers/learningPaths.ts | 59 ------------- backend/src/exceptions.ts | 21 +++++ ...learningContent.ts => learning-content.ts} | 17 +++- ...learningObjects.ts => learning-objects.ts} | 10 ++- .../{learningPaths.ts => learning-paths.ts} | 2 +- .../dwengo-api-learning-object-provider.ts | 87 +++++++++++-------- .../dwengo-api-learning-path-provider.ts | 4 +- .../learning-object-provider.ts | 17 +++- .../learning-object-service.ts | 30 +++++-- backend/src/util/apiHelper.ts | 12 ++- backend/src/util/envvars.ts | 2 + 16 files changed, 264 insertions(+), 178 deletions(-) create mode 100644 backend/src/controllers/learning-objects.ts create mode 100644 backend/src/controllers/learning-paths.ts delete mode 100644 backend/src/controllers/learningObjects.ts delete mode 100644 backend/src/controllers/learningPaths.ts create mode 100644 backend/src/exceptions.ts rename backend/src/interfaces/{learningContent.ts => learning-content.ts} (86%) rename backend/src/routes/{learningObjects.ts => learning-objects.ts} (68%) rename backend/src/routes/{learningPaths.ts => learning-paths.ts} (91%) diff --git a/backend/src/app.ts b/backend/src/app.ts index b21bb9f1..123075ea 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -3,8 +3,8 @@ import { initORM } from './orm.js'; import { EnvVars, getNumericEnvVar } from './util/envvars.js'; import themeRoutes from './routes/themes.js'; -import learningPathRoutes from './routes/learningPaths.js'; -import learningObjectRoutes from './routes/learningObjects.js'; +import learningPathRoutes from './routes/learning-paths.js'; +import learningObjectRoutes from './routes/learning-objects.js'; import studentRouter from './routes/student.js'; import groupRouter from './routes/group.js'; diff --git a/backend/src/config.ts b/backend/src/config.ts index 8fd8ec3f..2b9e67e3 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -5,6 +5,8 @@ // Load .env file // Dotenv.config(); -export const DWENGO_API_BASE = 'https://dwengo.org/backend/api'; +import {EnvVars, getEnvVar} from "./util/envvars"; -export const FALLBACK_LANG = 'nl'; +export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); + +export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts new file mode 100644 index 00000000..18bf5f26 --- /dev/null +++ b/backend/src/controllers/learning-objects.ts @@ -0,0 +1,62 @@ +import { Request, Response } from 'express'; +import { FALLBACK_LANG } from '../config.js'; +import {FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier} from '../interfaces/learning-content'; +import learningObjectService from "../services/learning-content/learning-object-service"; +import {EnvVars, getEnvVar} from "../util/envvars"; +import {Language} from "../entities/content/language"; +import {BadRequestException} from "../exceptions"; + +function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { + if (!req.params.hruid) { + throw new BadRequestException("HRUID is required."); + } + return { + hruid: req.params.hruid as string, + language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language, + version: req.query.version as string + }; +} + +function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentifier { + if (!req.query.hruid) { + throw new BadRequestException("HRUID is required."); + } + return { + hruid: req.params.hruid as string, + language: (req.query.language as Language) || FALLBACK_LANG + } +} + +export async function getAllLearningObjects( + req: Request, + res: Response +): Promise { + const learningPathId = getLearningPathIdentifierFromRequest(req); + const full = req.query.full; + + let learningObjects: FilteredLearningObject[] | string[]; + if (full) { + learningObjects = await learningObjectService.getLearningObjectsFromPath(learningPathId); + } else { + learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); + } + + res.json(learningObjects); +} + +export async function getLearningObject( + req: Request, + res: Response +): Promise { + const learningObjectId = getLearningObjectIdentifierFromRequest(req); + + const learningObject = await learningObjectService.getLearningObjectById(learningObjectId); + res.json(learningObject); +} + +export async function getLearningObjectHTML(req: Request, res: Response): Promise { + const learningObjectId = getLearningObjectIdentifierFromRequest(req); + + const learningObject = await learningObjectService.getLearningObjectHTML(learningObjectId); + res.send(learningObject); +} diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts new file mode 100644 index 00000000..593fd695 --- /dev/null +++ b/backend/src/controllers/learning-paths.ts @@ -0,0 +1,53 @@ +import { Request, Response } from 'express'; +import { themes } from '../data/themes.js'; +import { FALLBACK_LANG } from '../config.js'; +import learningPathService from "../services/learning-content/learning-path-service"; +import {NotFoundException} from "../exceptions"; + +/** + * Fetch learning paths based on query parameters. + */ +export async function getLearningPaths( + req: Request, + res: Response +): Promise { + const hruids = req.query.hruid; + const themeKey = req.query.theme as string; + const searchQuery = req.query.search as string; + const language = (req.query.language as string) || FALLBACK_LANG; + + let hruidList; + + if (hruids) { + hruidList = Array.isArray(hruids) + ? hruids.map(String) + : [String(hruids)]; + } else if (themeKey) { + const theme = themes.find((t) => { + return t.title === themeKey; + }); + if (theme) { + hruidList = theme.hruids; + } else { + throw new NotFoundException(`Theme "${themeKey}" not found.`); + } + } else if (searchQuery) { + const searchResults = await learningPathService.searchLearningPaths( + searchQuery, + language + ); + res.json(searchResults); + return; + } else { + hruidList = themes.flatMap((theme) => { + return theme.hruids; + }); + } + + const learningPaths = await learningPathService.fetchLearningPaths( + hruidList, + language, + `HRUIDs: ${hruidList.join(', ')}` + ); + res.json(learningPaths.data); +} diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts deleted file mode 100644 index 2455b49c..00000000 --- a/backend/src/controllers/learningObjects.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Request, Response } from 'express'; -import { FALLBACK_LANG } from '../config.js'; -import { FilteredLearningObject } from '../interfaces/learningContent'; -import learningObjectService from "../services/learning-content/learning-object-service"; - -export async function getAllLearningObjects( - req: Request, - res: Response -): Promise { - try { - const hruid = req.query.hruid as string; - const full = req.query.full === 'true'; - const language = (req.query.language as string) || FALLBACK_LANG; - - if (!hruid) { - res.status(400).json({ error: 'HRUID query is required.' }); - return; - } - - let learningObjects: FilteredLearningObject[] | string[]; - if (full) { - learningObjects = await learningObjectService.getLearningObjectsFromPath(hruid, language); - } else { - learningObjects = await learningObjectService.getLearningObjectIdsFromPath( - hruid, - language - ); - } - - res.json(learningObjects); - } catch (error) { - console.error('Error fetching learning objects:', error); - res.status(500).json({ error: 'Internal server error' }); - } -} - -export async function getLearningObject( - req: Request, - res: Response -): Promise { - try { - const { hruid } = req.params; - const language = (req.query.language as string) || FALLBACK_LANG; - - if (!hruid) { - res.status(400).json({ error: 'HRUID parameter is required.' }); - return; - } - - const learningObject = await learningObjectService.getLearningObjectById(hruid, language); - res.json(learningObject); - } catch (error) { - console.error('Error fetching learning object:', error); - res.status(500).json({ error: 'Internal server error' }); - } -} diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts deleted file mode 100644 index af521157..00000000 --- a/backend/src/controllers/learningPaths.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Request, Response } from 'express'; -import { themes } from '../data/themes.js'; -import { FALLBACK_LANG } from '../config.js'; -import learningPathService from "../services/learning-content/learning-path-service"; -/** - * Fetch learning paths based on query parameters. - */ -export async function getLearningPaths( - req: Request, - res: Response -): Promise { - try { - const hruids = req.query.hruid; - const themeKey = req.query.theme as string; - const searchQuery = req.query.search as string; - const language = (req.query.language as string) || FALLBACK_LANG; - - let hruidList; - - if (hruids) { - hruidList = Array.isArray(hruids) - ? hruids.map(String) - : [String(hruids)]; - } else if (themeKey) { - const theme = themes.find((t) => { - return t.title === themeKey; - }); - if (theme) { - hruidList = theme.hruids; - } else { - res.status(404).json({ - error: `Theme "${themeKey}" not found.`, - }); - return; - } - } else if (searchQuery) { - const searchResults = await learningPathService.searchLearningPaths( - searchQuery, - language - ); - res.json(searchResults); - return; - } else { - hruidList = themes.flatMap((theme) => { - return theme.hruids; - }); - } - - const learningPaths = await learningPathService.fetchLearningPaths( - hruidList, - language, - `HRUIDs: ${hruidList.join(', ')}` - ); - res.json(learningPaths.data); - } catch (error) { - console.error('❌ Unexpected error fetching learning paths:', error); - res.status(500).json({ error: 'Internal server error' }); - } -} diff --git a/backend/src/exceptions.ts b/backend/src/exceptions.ts new file mode 100644 index 00000000..993a13ba --- /dev/null +++ b/backend/src/exceptions.ts @@ -0,0 +1,21 @@ +/** + * Exception for HTTP 400 Bad Request + */ +export class BadRequestException extends Error { + public status = 400; + + constructor(error: string) { + super(error); + } +} + +/** + * Exception for HTTP 404 Not Found + */ +export class NotFoundException extends Error { + public status = 404; + + constructor(error: string) { + super(error); + } +} diff --git a/backend/src/interfaces/learningContent.ts b/backend/src/interfaces/learning-content.ts similarity index 86% rename from backend/src/interfaces/learningContent.ts rename to backend/src/interfaces/learning-content.ts index 9811543e..7fd3a55d 100644 --- a/backend/src/interfaces/learningContent.ts +++ b/backend/src/interfaces/learning-content.ts @@ -1,3 +1,5 @@ +import {Language} from "../entities/content/language"; + export interface Transition { default: boolean; _id: string; @@ -9,6 +11,12 @@ export interface Transition { }; } +export interface LearningObjectIdentifier { + hruid: string; + language: Language; + version?: string; +} + export interface LearningObjectNode { _id: string; learningobject_hruid: string; @@ -20,7 +28,7 @@ export interface LearningObjectNode { updatedAt: string; } -export interface LearningContent { +export interface LearningPath { _id: string; language: string; hruid: string; @@ -37,6 +45,11 @@ export interface LearningContent { __order: number; } +export interface LearningPathIdentifier { + hruid: string; + language: Language; +} + export interface EducationalGoal { source: string; id: string; @@ -93,6 +106,6 @@ export interface FilteredLearningObject { export interface LearningPathResponse { success: boolean; source: string; - data: LearningContent[] | null; + data: LearningPath[] | null; message?: string; } diff --git a/backend/src/routes/learningObjects.ts b/backend/src/routes/learning-objects.ts similarity index 68% rename from backend/src/routes/learningObjects.ts rename to backend/src/routes/learning-objects.ts index 416602b5..b44a6b8f 100644 --- a/backend/src/routes/learningObjects.ts +++ b/backend/src/routes/learning-objects.ts @@ -1,8 +1,8 @@ import express from 'express'; import { getAllLearningObjects, - getLearningObject, -} from '../controllers/learningObjects.js'; + getLearningObject, getLearningObjectHTML, +} from '../controllers/learning-objects.js'; const router = express.Router(); @@ -24,4 +24,10 @@ router.get('/', getAllLearningObjects); // Example: http://localhost:3000/learningObject/un_ai7 router.get('/:hruid', getLearningObject); +// Parameter: hruid of learning object +// Query: language, version (optional) +// Route to fetch the HTML rendering of one learning object based on its hruid. +// Example: http://localhost:3000/learningObject/un_ai7/html +router.get('/:hruid/html', getLearningObjectHTML); + export default router; diff --git a/backend/src/routes/learningPaths.ts b/backend/src/routes/learning-paths.ts similarity index 91% rename from backend/src/routes/learningPaths.ts rename to backend/src/routes/learning-paths.ts index ce580745..efe17312 100644 --- a/backend/src/routes/learningPaths.ts +++ b/backend/src/routes/learning-paths.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { getLearningPaths } from '../controllers/learningPaths.js'; +import { getLearningPaths } from '../controllers/learning-paths.js'; const router = express.Router(); diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts index c5e52695..981e073b 100644 --- a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts +++ b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts @@ -1,11 +1,11 @@ import { DWENGO_API_BASE } from '../../../config.js'; import { fetchWithLogging } from '../../../util/apiHelper.js'; import { - FilteredLearningObject, + FilteredLearningObject, LearningObjectIdentifier, LearningObjectMetadata, - LearningObjectNode, + LearningObjectNode, LearningPathIdentifier, LearningPathResponse, -} from '../../../interfaces/learningContent.js'; +} from '../../../interfaces/learning-content.js'; import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; import {LearningObjectProvider} from "../learning-object-provider"; @@ -13,11 +13,9 @@ import {LearningObjectProvider} from "../learning-object-provider"; * Helper function to convert the learning object metadata retrieved from the API to a FilteredLearningObject which * our API should return. * @param data - * @param htmlUrl */ function filterData( - data: LearningObjectMetadata, - htmlUrl: string + data: LearningObjectMetadata ): FilteredLearningObject { return { key: data.hruid, // Hruid learningObject (not path) @@ -25,7 +23,7 @@ function filterData( uuid: data.uuid, version: data.version, title: data.title, - htmlUrl, // Url to fetch html content + htmlUrl: `/learningObject/${data.hruid}/html?language=${data.language}&version=${data.version}`, // Url to fetch html content language: data.language, difficulty: data.difficulty, estimatedTime: data.estimated_time, @@ -43,19 +41,18 @@ function filterData( } /** - * Generic helper function to fetch learning objects (full data or just HRUIDs) + * Generic helper function to fetch all learning objects from a given path (full data or just HRUIDs) */ async function fetchLearningObjects( - hruid: string, - full: boolean, - language: string + learningPathId: LearningPathIdentifier, + full: boolean ): Promise { try { const learningPathResponse: LearningPathResponse = await dwengoApiLearningPathProvider.fetchLearningPaths( - [hruid], - language, - `Learning path for HRUID "${hruid}"` + [learningPathId.hruid], + learningPathId.language, + `Learning path for HRUID "${learningPathId.hruid}"` ); if ( @@ -63,7 +60,7 @@ async function fetchLearningObjects( !learningPathResponse.data?.length ) { console.error( - `⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.` + `⚠️ WARNING: Learning path "${learningPathId.hruid}" exists but contains no learning objects.` ); return []; } @@ -78,10 +75,10 @@ async function fetchLearningObjects( return await Promise.all( nodes.map(async (node) => { - return dwengoApiLearningObjectProvider.getLearningObjectById( - node.learningobject_hruid, - language - ); + return dwengoApiLearningObjectProvider.getLearningObjectById({ + hruid: node.learningobject_hruid, + language: learningPathId.language + }); }) ).then((objects) => { return objects.filter((obj): obj is FilteredLearningObject => { @@ -99,46 +96,62 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { * Fetches a single learning object by its HRUID */ async getLearningObjectById( - hruid: string, - language: string + id: LearningObjectIdentifier ): Promise { - const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; + let metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; const metadata = await fetchWithLogging( metadataUrl, - `Metadata for Learning Object HRUID "${hruid}" (language ${language})` + `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, + { + params: id + } ); if (!metadata) { - console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); + console.error(`⚠️ WARNING: Learning object "${id.hruid}" not found.`); return null; } - const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`; - return filterData(metadata, htmlUrl); + return filterData(metadata); }, /** * Fetch full learning object data (metadata) */ - async getLearningObjectsFromPath( - hruid: string, - language: string - ): Promise { + async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { return (await fetchLearningObjects( - hruid, + id, true, - language )) as FilteredLearningObject[]; }, /** * Fetch only learning object HRUIDs */ - async getLearningObjectIdsFromPath( - hruid: string, - language: string - ): Promise { - return (await fetchLearningObjects(hruid, false, language)) as string[]; + async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { + return (await fetchLearningObjects(id, false)) as string[]; + }, + + /** + * Obtain a HTML-rendering of the learning object with the given identifier (as a string). For learning objects + * from the Dwengo API, this means passing through the HTML rendering from there. + */ + async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; + const html = await fetchWithLogging( + htmlUrl, + `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, + { + params: id + } + ); + + if (!html) { + console.error(`⚠️ WARNING: Learning object "${id.hruid}" not found.`); + return null; + } + + return html; } }; diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts index f7b4ff7d..5811398c 100644 --- a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts @@ -27,7 +27,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { const learningPaths = await fetchWithLogging( apiUrl, `Learning paths for ${source}`, - params + { params } ); if (!learningPaths || learningPaths.length === 0) { @@ -56,7 +56,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { const searchResults = await fetchWithLogging( apiUrl, `Search learning paths with query "${query}"`, - params + { params } ); return searchResults ?? []; } diff --git a/backend/src/services/learning-content/learning-object-provider.ts b/backend/src/services/learning-content/learning-object-provider.ts index e37c4442..70190a1a 100644 --- a/backend/src/services/learning-content/learning-object-provider.ts +++ b/backend/src/services/learning-content/learning-object-provider.ts @@ -1,18 +1,27 @@ -import {FilteredLearningObject} from "../../interfaces/learningContent"; +import { + FilteredLearningObject, + LearningObjectIdentifier, + LearningPathIdentifier +} from "../../interfaces/learning-content"; export interface LearningObjectProvider { /** * Fetches a single learning object by its HRUID */ - getLearningObjectById(hruid: string, language: string): Promise; + getLearningObjectById(id: LearningObjectIdentifier): Promise; /** * Fetch full learning object data (metadata) */ - getLearningObjectsFromPath(hruid: string, language: string): Promise; + getLearningObjectsFromPath(id: LearningPathIdentifier): Promise; /** * Fetch only learning object HRUIDs */ - getLearningObjectIdsFromPath(hruid: string, language: string): Promise; + getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise; + + /** + * Obtain a HTML-rendering of the learning object with the given identifier (as a string). + */ + getLearningObjectHTML(id: LearningObjectIdentifier): Promise; } diff --git a/backend/src/services/learning-content/learning-object-service.ts b/backend/src/services/learning-content/learning-object-service.ts index e0beec60..4daedb79 100644 --- a/backend/src/services/learning-content/learning-object-service.ts +++ b/backend/src/services/learning-content/learning-object-service.ts @@ -1,5 +1,14 @@ -import {FilteredLearningObject} from "../../interfaces/learningContent"; +import { + FilteredLearningObject, + LearningObjectIdentifier, + LearningPathIdentifier +} from "../../interfaces/learning-content"; import dwengoApiLearningObjectProvider from "./dwengo-api/dwengo-api-learning-object-provider"; +import {LearningObjectProvider} from "./learning-object-provider"; + +function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { + return dwengoApiLearningObjectProvider +} /** * Service providing access to data about learning objects from the appropriate data source (database or Dwengo-api) @@ -8,22 +17,29 @@ const learningObjectService = { /** * Fetches a single learning object by its HRUID */ - getLearningObjectById(hruid: string, language: string): Promise { - return dwengoApiLearningObjectProvider.getLearningObjectById(hruid, language); + getLearningObjectById(id: LearningObjectIdentifier): Promise { + return getProvider(id).getLearningObjectById(id); }, /** * Fetch full learning object data (metadata) */ - getLearningObjectsFromPath(hruid: string, language: string): Promise { - return dwengoApiLearningObjectProvider.getLearningObjectsFromPath(hruid, language); + getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { + return getProvider(id).getLearningObjectsFromPath(id); }, /** * Fetch only learning object HRUIDs */ - getLearningObjectIdsFromPath(hruid: string, language: string): Promise { - return dwengoApiLearningObjectProvider.getLearningObjectIdsFromPath(hruid, language); + getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { + return getProvider(id).getLearningObjectIdsFromPath(id); + }, + + /** + * Obtain a HTML-rendering of the learning object with the given identifier (as a string). + */ + getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + return getProvider(id).getLearningObjectHTML(id); } }; diff --git a/backend/src/util/apiHelper.ts b/backend/src/util/apiHelper.ts index 76d166c8..8733a7a4 100644 --- a/backend/src/util/apiHelper.ts +++ b/backend/src/util/apiHelper.ts @@ -8,17 +8,21 @@ import axios, { AxiosRequestConfig } from 'axios'; * * @param url The API endpoint to fetch from. * @param description A short description of what is being fetched (for logging). - * @param params + * @param options Contains further options such as params (the query params) and responseType (whether the response + * should be parsed as JSON ("json") or whether it should be returned as plain text ("text") * @returns The response data if successful, or null if an error occurs. */ export async function fetchWithLogging( url: string, description: string, - params?: Record + options?: { + params?: Record, + query?: Record, + responseType?: "json" | "text", + } ): Promise { try { - const config: AxiosRequestConfig = params ? { params } : {}; - + const config: AxiosRequestConfig = options || {}; const response = await axios.get(url, config); return response.data; } catch (error: any) { diff --git a/backend/src/util/envvars.ts b/backend/src/util/envvars.ts index 5a06ac22..8cb82487 100644 --- a/backend/src/util/envvars.ts +++ b/backend/src/util/envvars.ts @@ -11,6 +11,8 @@ export const EnvVars: { [key: string]: EnvVar } = { DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, + LearningContentRepoApiBaseUrl: { key: PREFIX + "LEARNING_CONTENT_REPO_API_BASE_URL", defaultValue: "https://dwengo.org/backend/api"}, + FallbackLanguage: { key: PREFIX + "FALLBACK_LANGUAGE", defaultValue: "nl" }, } as const; /** From cc6947dd3c971e4f6a568a10278ff81e77ade3b0 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 4 Mar 2025 22:38:21 +0100 Subject: [PATCH 006/119] refactor(backend): Changed folder structure for learning content services. --- backend/src/controllers/learning-objects.ts | 2 +- backend/src/controllers/learning-paths.ts | 2 +- .../dwengo-api-learning-object-provider.ts | 10 +++++----- .../learning-object-provider.ts | 0 .../learning-object-service.ts | 2 +- .../dwengo-api-learning-path-provider.ts | 8 ++++---- .../learning-path-provider.ts | 0 .../learning-path-service.ts | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) rename backend/src/services/{learning-content/dwengo-api => learning-objects}/dwengo-api-learning-object-provider.ts (94%) rename backend/src/services/{learning-content => learning-objects}/learning-object-provider.ts (100%) rename backend/src/services/{learning-content => learning-objects}/learning-object-service.ts (93%) rename backend/src/services/{learning-content/dwengo-api => learning-paths}/dwengo-api-learning-path-provider.ts (88%) rename backend/src/services/{learning-content => learning-paths}/learning-path-provider.ts (100%) rename backend/src/services/{learning-content => learning-paths}/learning-path-service.ts (90%) diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index 18bf5f26..c4dd8d08 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import { FALLBACK_LANG } from '../config.js'; import {FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier} from '../interfaces/learning-content'; -import learningObjectService from "../services/learning-content/learning-object-service"; +import learningObjectService from "../services/learning-objects/learning-object-service"; import {EnvVars, getEnvVar} from "../util/envvars"; import {Language} from "../entities/content/language"; import {BadRequestException} from "../exceptions"; diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts index 593fd695..cb5ae07e 100644 --- a/backend/src/controllers/learning-paths.ts +++ b/backend/src/controllers/learning-paths.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; import { FALLBACK_LANG } from '../config.js'; -import learningPathService from "../services/learning-content/learning-path-service"; +import learningPathService from "../services/learning-paths/learning-path-service"; import {NotFoundException} from "../exceptions"; /** diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts similarity index 94% rename from backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts rename to backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts index 981e073b..bef08dfe 100644 --- a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts +++ b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts @@ -1,13 +1,13 @@ -import { DWENGO_API_BASE } from '../../../config.js'; -import { fetchWithLogging } from '../../../util/apiHelper.js'; +import { DWENGO_API_BASE } from '../../config.js'; +import { fetchWithLogging } from '../../util/apiHelper.js'; import { FilteredLearningObject, LearningObjectIdentifier, LearningObjectMetadata, LearningObjectNode, LearningPathIdentifier, LearningPathResponse, -} from '../../../interfaces/learning-content.js'; -import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; -import {LearningObjectProvider} from "../learning-object-provider"; +} from '../../interfaces/learning-content.js'; +import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; +import {LearningObjectProvider} from "./learning-object-provider"; /** * Helper function to convert the learning object metadata retrieved from the API to a FilteredLearningObject which diff --git a/backend/src/services/learning-content/learning-object-provider.ts b/backend/src/services/learning-objects/learning-object-provider.ts similarity index 100% rename from backend/src/services/learning-content/learning-object-provider.ts rename to backend/src/services/learning-objects/learning-object-provider.ts diff --git a/backend/src/services/learning-content/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts similarity index 93% rename from backend/src/services/learning-content/learning-object-service.ts rename to backend/src/services/learning-objects/learning-object-service.ts index 4daedb79..587e8968 100644 --- a/backend/src/services/learning-content/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -3,7 +3,7 @@ import { LearningObjectIdentifier, LearningPathIdentifier } from "../../interfaces/learning-content"; -import dwengoApiLearningObjectProvider from "./dwengo-api/dwengo-api-learning-object-provider"; +import dwengoApiLearningObjectProvider from "./dwengo-api-learning-object-provider"; import {LearningObjectProvider} from "./learning-object-provider"; function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts similarity index 88% rename from backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts rename to backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts index 5811398c..ae6dff0a 100644 --- a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts @@ -1,10 +1,10 @@ -import { fetchWithLogging } from '../../../util/apiHelper.js'; -import { DWENGO_API_BASE } from '../../../config.js'; +import { fetchWithLogging } from '../../util/apiHelper.js'; +import { DWENGO_API_BASE } from '../../config.js'; import { LearningPath, LearningPathResponse, -} from '../../../interfaces/learning-content.js'; -import {LearningPathProvider} from "../learning-path-provider"; +} from '../../interfaces/learning-content.js'; +import {LearningPathProvider} from "./learning-path-provider"; const dwengoApiLearningPathProvider: LearningPathProvider = { async fetchLearningPaths( diff --git a/backend/src/services/learning-content/learning-path-provider.ts b/backend/src/services/learning-paths/learning-path-provider.ts similarity index 100% rename from backend/src/services/learning-content/learning-path-provider.ts rename to backend/src/services/learning-paths/learning-path-provider.ts diff --git a/backend/src/services/learning-content/learning-path-service.ts b/backend/src/services/learning-paths/learning-path-service.ts similarity index 90% rename from backend/src/services/learning-content/learning-path-service.ts rename to backend/src/services/learning-paths/learning-path-service.ts index 0bb925bf..fd37cd38 100644 --- a/backend/src/services/learning-content/learning-path-service.ts +++ b/backend/src/services/learning-paths/learning-path-service.ts @@ -1,5 +1,5 @@ import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; -import dwengoApiLearningPathProvider from "./dwengo-api/dwengo-api-learning-path-provider"; +import dwengoApiLearningPathProvider from "./dwengo-api-learning-path-provider"; /** * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) From 2d9f17484c54b14798358df29cd67dd835154afc Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 4 Mar 2025 23:33:03 +0100 Subject: [PATCH 007/119] feat(backend): Added skeleton for a database learning object/path provider. --- .../database-learning-object-provider.ts | 42 +++++++++++++++++++ .../learning-object-service.ts | 8 +++- .../database-learning-path-provider.ts | 23 ++++++++++ .../learning-paths/learning-path-service.ts | 35 +++++++++++++--- backend/src/util/envvars.ts | 1 + 5 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 backend/src/services/learning-objects/database-learning-object-provider.ts create mode 100644 backend/src/services/learning-paths/database-learning-path-provider.ts diff --git a/backend/src/services/learning-objects/database-learning-object-provider.ts b/backend/src/services/learning-objects/database-learning-object-provider.ts new file mode 100644 index 00000000..85b97614 --- /dev/null +++ b/backend/src/services/learning-objects/database-learning-object-provider.ts @@ -0,0 +1,42 @@ +import {LearningObjectProvider} from "./learning-object-provider"; +import { + FilteredLearningObject, + LearningObjectIdentifier, + LearningPathIdentifier +} from "../../interfaces/learning-content"; + +/** + * Service providing access to data about learning objects from the database + */ +const databaseLearningObjectProvider: LearningObjectProvider = { + /** + * Fetches a single learning object by its HRUID + */ + getLearningObjectById(id: LearningObjectIdentifier): Promise { + return Promise.resolve(null); // TODO + }, + + /** + * Fetch full learning object data (metadata) + */ + getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + return Promise.resolve(null); // TODO + }, + + /** + * Fetch only learning object HRUIDs + */ + getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { + return Promise.resolve([]);// TODO + }, + + /** + * Obtain a HTML-rendering of the learning object with the given identifier (as a string). + */ + getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { + return Promise.resolve([]); // TODO + } + +} + +export default databaseLearningObjectProvider; diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 587e8968..51152ec8 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -5,9 +5,15 @@ import { } from "../../interfaces/learning-content"; import dwengoApiLearningObjectProvider from "./dwengo-api-learning-object-provider"; import {LearningObjectProvider} from "./learning-object-provider"; +import {EnvVars, getEnvVar} from "../../util/envvars"; +import databaseLearningObjectProvider from "./database-learning-object-provider"; function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { - return dwengoApiLearningObjectProvider + if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) { + return databaseLearningObjectProvider; + } else { + return dwengoApiLearningObjectProvider; + } } /** diff --git a/backend/src/services/learning-paths/database-learning-path-provider.ts b/backend/src/services/learning-paths/database-learning-path-provider.ts new file mode 100644 index 00000000..292bb79e --- /dev/null +++ b/backend/src/services/learning-paths/database-learning-path-provider.ts @@ -0,0 +1,23 @@ +import {LearningPathProvider} from "./learning-path-provider"; +import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; + +/** + * Service providing access to data about learning paths from the database. + */ +const databaseLearningPathProvider: LearningPathProvider = { + /** + * Fetch the learning paths with the given hruids from the database. + */ + fetchLearningPaths(hruids: string[], language: string, source: string): Promise { + throw new Error("Not yet implemented"); // TODO + }, + + /** + * Search learning paths in the database using the given search string. + */ + searchLearningPaths(query: string, language: string): Promise { + return Promise.resolve([]); // TODO + } +} + +export default databaseLearningPathProvider; diff --git a/backend/src/services/learning-paths/learning-path-service.ts b/backend/src/services/learning-paths/learning-path-service.ts index fd37cd38..a2776554 100644 --- a/backend/src/services/learning-paths/learning-path-service.ts +++ b/backend/src/services/learning-paths/learning-path-service.ts @@ -1,5 +1,13 @@ -import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; +import { + LearningPath, + LearningPathResponse +} from "../../interfaces/learning-content"; import dwengoApiLearningPathProvider from "./dwengo-api-learning-path-provider"; +import databaseLearningPathProvider from "./database-learning-path-provider"; +import {EnvVars, getEnvVar} from "../../util/envvars"; + +const userContentPrefix = getEnvVar(EnvVars.UserContentPrefix); +const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider] /** * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) @@ -8,15 +16,32 @@ const learningPathService = { /** * Fetch the learning paths with the given hruids from the data source. */ - fetchLearningPaths(hruids: string[], language: string, source: string): Promise { - return dwengoApiLearningPathProvider.fetchLearningPaths(hruids, language, source); + async fetchLearningPaths(hruids: string[], language: string, source: string): Promise { + const userContentHruids = hruids.filter(hruid => hruid.startsWith(userContentPrefix)); + const nonUserContentHruids = hruids.filter(hruid => !hruid.startsWith(userContentPrefix)); + + const userContentLearningPaths = + await databaseLearningPathProvider.fetchLearningPaths(userContentHruids, language, source); + const nonUserContentLearningPaths + = await databaseLearningPathProvider.fetchLearningPaths(nonUserContentHruids, language, source); + + return { + data: (userContentLearningPaths.data || []).concat(nonUserContentLearningPaths.data || []), + source: source, + success: userContentLearningPaths.success || nonUserContentLearningPaths.success + }; }, /** * Search learning paths in the data source using the given search string. */ - searchLearningPaths(query: string, language: string): Promise { - return dwengoApiLearningPathProvider.searchLearningPaths(query, language); + async searchLearningPaths(query: string, language: string): Promise { + const providerResponses = await Promise.all( + allProviders.map( + provider => provider.searchLearningPaths(query, language) + ) + ); + return providerResponses.flat(); } } diff --git a/backend/src/util/envvars.ts b/backend/src/util/envvars.ts index 8cb82487..c6258a30 100644 --- a/backend/src/util/envvars.ts +++ b/backend/src/util/envvars.ts @@ -13,6 +13,7 @@ export const EnvVars: { [key: string]: EnvVar } = { DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, LearningContentRepoApiBaseUrl: { key: PREFIX + "LEARNING_CONTENT_REPO_API_BASE_URL", defaultValue: "https://dwengo.org/backend/api"}, FallbackLanguage: { key: PREFIX + "FALLBACK_LANGUAGE", defaultValue: "nl" }, + UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: "u_" }, } as const; /** From 5a7bbf9d9ca3bcd6360a42ec667d035892e9c9db Mon Sep 17 00:00:00 2001 From: Joyelle Ndagijimana Date: Thu, 6 Mar 2025 13:36:54 +0100 Subject: [PATCH 008/119] feat(frontend): 'en' en 'nl' vertalingen zijn nu beschikbaar via de menubalk. --- frontend/src/components/MenuBar.vue | 14 +++++++++----- frontend/src/i18n/i18n.ts | 25 +++++++++++++------------ frontend/src/i18n/locale/de.json | 3 +++ frontend/src/i18n/locale/en.json | 9 +++++++++ frontend/src/i18n/locale/fr.json | 3 +++ frontend/src/i18n/locale/nl.json | 9 +++++++++ frontend/src/i18n/locales/de.json | 0 frontend/src/i18n/locales/en.json | 0 frontend/src/i18n/locales/fr.json | 0 frontend/src/i18n/locales/nl.json | 0 10 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 frontend/src/i18n/locale/de.json create mode 100644 frontend/src/i18n/locale/en.json create mode 100644 frontend/src/i18n/locale/fr.json create mode 100644 frontend/src/i18n/locale/nl.json delete mode 100644 frontend/src/i18n/locales/de.json delete mode 100644 frontend/src/i18n/locales/en.json delete mode 100644 frontend/src/i18n/locales/fr.json delete mode 100644 frontend/src/i18n/locales/nl.json diff --git a/frontend/src/components/MenuBar.vue b/frontend/src/components/MenuBar.vue index fe2b3563..e6d6df5d 100644 --- a/frontend/src/components/MenuBar.vue +++ b/frontend/src/components/MenuBar.vue @@ -2,8 +2,10 @@ import { ref } from "vue"; import { useRoute } from "vue-router"; import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; + import {useI18n} from "vue-i18n"; const route = useRoute(); + const { t, locale } = useI18n() // Instantiate variables to use in html to render right // Links and content dependent on the role (student or teacher) @@ -27,6 +29,8 @@ // Logic to change the language of the website to the selected language const changeLanguage = (langCode: string) => { + locale.value = langCode; + localStorage.setItem('user-lang', langCode); console.log(langCode); }; @@ -46,7 +50,7 @@ :src="dwengoLogo" />

- {{ role }} + {{ t(`${role}`) }}

@@ -55,21 +59,21 @@ :to="`/${role}/${userId}/assignment`" class="menu_item" > - assignments + {{ t('assignments') }}
  • classes{{ t('classes') }}
  • discussions{{ t('discussions') }}
  • @@ -104,7 +108,7 @@