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..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 @@ -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,24 @@ 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..7651baa3 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); } /** 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..6cccc37c 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..c858ac26 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,10 @@ 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); }); });