From eae4ad39786d8fb53da18942d417622dc34b74f4 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Wed, 14 May 2025 09:23:39 +0200 Subject: [PATCH 1/4] feat(backend): Progess voor leerpaden uit de Dwengo API --- .../database-learning-path-provider.ts | 4 +- .../dwengo-api-learning-path-provider.ts | 38 ++++++++++++++++++- .../learning-path-personalization-util.ts | 28 +++++++++++--- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/backend/src/services/learning-paths/database-learning-path-provider.ts b/backend/src/services/learning-paths/database-learning-path-provider.ts index fe05dda1..eb980fa1 100644 --- a/backend/src/services/learning-paths/database-learning-path-provider.ts +++ b/backend/src/services/learning-paths/database-learning-path-provider.ts @@ -4,7 +4,7 @@ import { getLearningPathRepository } from '../../data/repositories.js'; import learningObjectService from '../learning-objects/learning-object-service.js'; import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; -import { getLastSubmissionForGroup, isTransitionPossible } from './learning-path-personalization-util.js'; +import { getLastSubmissionForGroup, idFromLearningPathNode, isTransitionPossible } from './learning-path-personalization-util.js'; import { FilteredLearningObject, LearningObjectNode, @@ -95,7 +95,7 @@ async function convertNode( personalizedFor: Group | undefined, nodesToLearningObjects: Map ): Promise { - const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(node, personalizedFor) : null; + const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningPathNode(node), personalizedFor) : null; const transitions = node.transitions .filter( (trans) => diff --git a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts index 110cd570..461ba7e8 100644 --- a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts @@ -3,11 +3,33 @@ import { DWENGO_API_BASE } from '../../config.js'; import { LearningPathProvider } from './learning-path-provider.js'; import { getLogger, Logger } from '../../logging/initalize.js'; import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; +import { Group } from '../../entities/assignments/group.entity.js'; +import { getLastSubmissionForGroup, idFromLearningObjectNode } from './learning-path-personalization-util.js'; const logger: Logger = getLogger(); +/** + * Adds progress information to the learning path. Modifies the learning path in-place. + * @param learningPath The learning path to add progress to. + * @param personalizedFor The group whose progress should be shown. + * @returns the modified learning path. + */ +async function addProgressToLearningPath(learningPath: LearningPath, personalizedFor: Group): Promise { + await Promise.all( + learningPath.nodes.map(async node => { + const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningObjectNode(node), personalizedFor) : null + node.done = Boolean(lastSubmission); + }) + ); + + learningPath.num_nodes = learningPath.nodes.length; + learningPath.num_nodes_left = learningPath.nodes.filter(it => !it.done).length; + + return learningPath; +} + const dwengoApiLearningPathProvider: LearningPathProvider = { - async fetchLearningPaths(hruids: string[], language: string, source: string): Promise { + async fetchLearningPaths(hruids: string[], language: string, source: string, personalizedFor: Group): Promise { if (hruids.length === 0) { return { success: false, @@ -32,17 +54,29 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { }; } + await Promise.all( + learningPaths?.map(async it => addProgressToLearningPath(it, personalizedFor)) + ); + return { success: true, source, data: learningPaths, }; }, - async searchLearningPaths(query: string, language: string): Promise { + async searchLearningPaths(query: string, language: string, personalizedFor: Group): 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 }); + + if (searchResults) { + await Promise.all( + searchResults?.map(async it => addProgressToLearningPath(it, personalizedFor)) + ); + } + + return searchResults ?? []; }, }; diff --git a/backend/src/services/learning-paths/learning-path-personalization-util.ts b/backend/src/services/learning-paths/learning-path-personalization-util.ts index a10d5ead..ab1f26bf 100644 --- a/backend/src/services/learning-paths/learning-path-personalization-util.ts +++ b/backend/src/services/learning-paths/learning-path-personalization-util.ts @@ -5,18 +5,36 @@ import { getSubmissionRepository } from '../../data/repositories.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; import { JSONPath } from 'jsonpath-plus'; +import { LearningObjectNode } from '@dwengo-1/common/interfaces/learning-content'; /** * Returns the last submission for the learning object associated with the given node and for the group */ -export async function getLastSubmissionForGroup(node: LearningPathNode, pathFor: Group): Promise { +export async function getLastSubmissionForGroup(learningObjectId: LearningObjectIdentifier, pathFor: Group): Promise { const submissionRepo = getSubmissionRepository(); - const learningObjectId: LearningObjectIdentifier = { + return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor); +} + +/** + * Creates a LearningObjectIdentifier describing the specified node. + */ +export function idFromLearningObjectNode(node: LearningObjectNode): LearningObjectIdentifier { + return { + hruid: node.learningobject_hruid, + language: node.language, + version: node.version + } +} + +/** + * Creates a LearningObjectIdentifier describing the specified node. + */ +export function idFromLearningPathNode(node: LearningPathNode): LearningObjectIdentifier { + return { hruid: node.learningObjectHruid, language: node.language, - version: node.version, - }; - return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor); + version: node.version + } } /** From 97d3c5c4802dc1495e848683a8190eeb739a6fb7 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Wed, 14 May 2025 07:30:54 +0000 Subject: [PATCH 2/4] style: fix linting issues met Prettier --- .../dwengo-api-learning-path-provider.ts | 15 +++++---------- .../learning-path-personalization-util.ts | 8 ++++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts index 461ba7e8..fd5b7f50 100644 --- a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts @@ -16,14 +16,14 @@ const logger: Logger = getLogger(); */ async function addProgressToLearningPath(learningPath: LearningPath, personalizedFor: Group): Promise { await Promise.all( - learningPath.nodes.map(async node => { - const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningObjectNode(node), personalizedFor) : null + learningPath.nodes.map(async (node) => { + const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningObjectNode(node), personalizedFor) : null; node.done = Boolean(lastSubmission); }) ); learningPath.num_nodes = learningPath.nodes.length; - learningPath.num_nodes_left = learningPath.nodes.filter(it => !it.done).length; + learningPath.num_nodes_left = learningPath.nodes.filter((it) => !it.done).length; return learningPath; } @@ -54,9 +54,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { }; } - await Promise.all( - learningPaths?.map(async it => addProgressToLearningPath(it, personalizedFor)) - ); + await Promise.all(learningPaths?.map(async (it) => addProgressToLearningPath(it, personalizedFor))); return { success: true, @@ -71,12 +69,9 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { const searchResults = await fetchWithLogging(apiUrl, `Search learning paths with query "${query}"`, { params }); if (searchResults) { - await Promise.all( - searchResults?.map(async it => addProgressToLearningPath(it, personalizedFor)) - ); + await Promise.all(searchResults?.map(async (it) => addProgressToLearningPath(it, personalizedFor))); } - return searchResults ?? []; }, }; diff --git a/backend/src/services/learning-paths/learning-path-personalization-util.ts b/backend/src/services/learning-paths/learning-path-personalization-util.ts index ab1f26bf..7651baa3 100644 --- a/backend/src/services/learning-paths/learning-path-personalization-util.ts +++ b/backend/src/services/learning-paths/learning-path-personalization-util.ts @@ -22,8 +22,8 @@ export function idFromLearningObjectNode(node: LearningObjectNode): LearningObje return { hruid: node.learningobject_hruid, language: node.language, - version: node.version - } + version: node.version, + }; } /** @@ -33,8 +33,8 @@ export function idFromLearningPathNode(node: LearningPathNode): LearningObjectId return { hruid: node.learningObjectHruid, language: node.language, - version: node.version - } + version: node.version, + }; } /** From a1c9f37081f15a0753e45d9e1c32b3658be3cb88 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Wed, 14 May 2025 09:41:09 +0200 Subject: [PATCH 3/4] fix(frontend): Leerpaden voor thema's opvragen in de juiste taal --- frontend/src/controllers/learning-paths.ts | 4 ++-- frontend/src/queries/learning-paths.ts | 7 ++++--- frontend/src/views/SingleTheme.vue | 5 +++-- .../tests/controllers/learning-paths-controller.test.ts | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/controllers/learning-paths.ts b/frontend/src/controllers/learning-paths.ts index bad54286..09a30feb 100644 --- a/frontend/src/controllers/learning-paths.ts +++ b/frontend/src/controllers/learning-paths.ts @@ -26,8 +26,8 @@ export class LearningPathController extends BaseController { }); return LearningPath.fromDTO(single(dtos)); } - async getAllByTheme(theme: string): Promise { - const dtos = await this.get("/", { theme }); + async getAllByThemeAndLanguage(theme: string, language: Language): Promise { + const dtos = await this.get("/", { theme, language }); return dtos.map((dto) => LearningPath.fromDTO(dto)); } diff --git a/frontend/src/queries/learning-paths.ts b/frontend/src/queries/learning-paths.ts index 1f088c9d..9f2424d7 100644 --- a/frontend/src/queries/learning-paths.ts +++ b/frontend/src/queries/learning-paths.ts @@ -22,12 +22,13 @@ export function useGetLearningPathQuery( }); } -export function useGetAllLearningPathsByThemeQuery( +export function useGetAllLearningPathsByThemeAndLanguageQuery( theme: MaybeRefOrGetter, + language: MaybeRefOrGetter ): UseQueryReturnType { return useQuery({ - queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme], - queryFn: async () => learningPathController.getAllByTheme(toValue(theme)), + queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme, language], + queryFn: async () => learningPathController.getAllByThemeAndLanguage(toValue(theme), toValue(language)), enabled: () => Boolean(toValue(theme)), }); } diff --git a/frontend/src/views/SingleTheme.vue b/frontend/src/views/SingleTheme.vue index 6924cc1c..23b0145b 100644 --- a/frontend/src/views/SingleTheme.vue +++ b/frontend/src/views/SingleTheme.vue @@ -2,10 +2,11 @@ import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; import LearningPathsGrid from "@/components/LearningPathsGrid.vue"; import UsingQueryResult from "@/components/UsingQueryResult.vue"; - import { useGetAllLearningPathsByThemeQuery } from "@/queries/learning-paths.ts"; + import { useGetAllLearningPathsByThemeAndLanguageQuery } from "@/queries/learning-paths.ts"; import { computed, ref } from "vue"; import { useI18n } from "vue-i18n"; import { useThemeQuery } from "@/queries/themes.ts"; +import type { Language } from "@/data-objects/language"; const props = defineProps<{ theme: string }>(); @@ -16,7 +17,7 @@ const currentThemeInfo = computed(() => themeQueryResult.data.value?.find((it) => it.key === props.theme)); - const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeQuery(() => props.theme); + const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeAndLanguageQuery(() => props.theme, () => locale.value as Language); const { t } = useI18n(); const searchFilter = ref(""); diff --git a/frontend/tests/controllers/learning-paths-controller.test.ts b/frontend/tests/controllers/learning-paths-controller.test.ts index 28e4cda2..c987c3a4 100644 --- a/frontend/tests/controllers/learning-paths-controller.test.ts +++ b/frontend/tests/controllers/learning-paths-controller.test.ts @@ -15,7 +15,7 @@ describe("Test controller learning paths", () => { }); it("Can get learning path by id", async () => { - const data = await controller.getAllByTheme("kiks"); + const data = await controller.getAllByThemeAndLanguage("kiks", Language.Dutch); expect(data).to.have.length.greaterThan(0); }); }); From 0d6e9020623676c51f095862658523b9fdedfc1f Mon Sep 17 00:00:00 2001 From: Lint Action Date: Wed, 14 May 2025 07:48:01 +0000 Subject: [PATCH 4/4] style: fix linting issues met Prettier --- frontend/src/queries/learning-paths.ts | 2 +- frontend/src/views/SingleTheme.vue | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/queries/learning-paths.ts b/frontend/src/queries/learning-paths.ts index 9f2424d7..6cccc37c 100644 --- a/frontend/src/queries/learning-paths.ts +++ b/frontend/src/queries/learning-paths.ts @@ -24,7 +24,7 @@ export function useGetLearningPathQuery( export function useGetAllLearningPathsByThemeAndLanguageQuery( theme: MaybeRefOrGetter, - language: MaybeRefOrGetter + language: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme, language], diff --git a/frontend/src/views/SingleTheme.vue b/frontend/src/views/SingleTheme.vue index 23b0145b..c858ac26 100644 --- a/frontend/src/views/SingleTheme.vue +++ b/frontend/src/views/SingleTheme.vue @@ -6,7 +6,7 @@ import { computed, ref } from "vue"; import { useI18n } from "vue-i18n"; import { useThemeQuery } from "@/queries/themes.ts"; -import type { Language } from "@/data-objects/language"; + import type { Language } from "@/data-objects/language"; const props = defineProps<{ theme: string }>(); @@ -17,7 +17,10 @@ import type { Language } from "@/data-objects/language"; const currentThemeInfo = computed(() => themeQueryResult.data.value?.find((it) => it.key === props.theme)); - const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeAndLanguageQuery(() => props.theme, () => locale.value as Language); + const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeAndLanguageQuery( + () => props.theme, + () => locale.value as Language, + ); const { t } = useI18n(); const searchFilter = ref("");