refactor(backend): Personalisatie van leerpaden is enkel mogelijk voor groepen, niet voor individuele studenten.

This commit is contained in:
Gerald Schmittinger 2025-04-14 17:14:43 +02:00
parent 1ccbfd6c38
commit 4092f1f617
8 changed files with 41 additions and 89 deletions

View file

@ -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 { getLastSubmissionForCustomizationTarget, isTransitionPossible, PersonalizationTarget } from './learning-path-personalization-util.js';
import { getLastSubmissionForGroup, isTransitionPossible } from './learning-path-personalization-util.js';
import {
FilteredLearningObject,
LearningObjectNode,
@ -13,6 +13,7 @@ import {
Transition,
} from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language';
import {Group} from "../../entities/assignments/group.entity";
/**
* Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its
@ -44,7 +45,7 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
/**
* Convert the given learning path entity to an object which conforms to the learning path content.
*/
async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: PersonalizationTarget): Promise<LearningPath> {
async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: Group): Promise<LearningPath> {
// Fetch the corresponding learning object for each node since some parts of the expected response contains parts
// With information which is not available in the LearningPathNodes themselves.
const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes);
@ -89,10 +90,10 @@ async function convertLearningPath(learningPath: LearningPathEntity, order: numb
async function convertNode(
node: LearningPathNode,
learningObject: FilteredLearningObject,
personalizedFor: PersonalizationTarget | undefined,
personalizedFor: Group | undefined,
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>
): Promise<LearningObjectNode> {
const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null;
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(node, personalizedFor) : null;
const transitions = node.transitions
.filter(
(trans) =>
@ -121,7 +122,7 @@ async function convertNode(
*/
async function convertNodes(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget
personalizedFor?: Group
): Promise<LearningObjectNode[]> {
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) =>
convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects)
@ -181,7 +182,7 @@ const databaseLearningPathProvider: LearningPathProvider = {
hruids: string[],
language: Language,
source: string,
personalizedFor?: PersonalizationTarget
personalizedFor?: Group
): Promise<LearningPathResponse> {
const learningPathRepo = getLearningPathRepository();
@ -202,7 +203,7 @@ const databaseLearningPathProvider: LearningPathProvider = {
/**
* Search learning paths in the database using the given search string.
*/
async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> {
async searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]> {
const learningPathRepo = getLearningPathRepository();
const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language);

View file

@ -1,76 +1,22 @@
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
import { Student } from '../../entities/users/student.entity.js';
import { Group } from '../../entities/assignments/group.entity.js';
import { Submission } from '../../entities/assignments/submission.entity.js';
import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../../data/repositories.js';
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';
export type PersonalizationTarget = { type: 'student'; student: Student } | { type: 'group'; group: Group };
/**
* Shortcut function to easily create a PersonalizationTarget object for a student by his/her username.
* @param username Username of the student we want to generate a personalized learning path for.
* If there is no student with this username, return undefined.
* Returns the last submission for the learning object associated with the given node and for the group
*/
export async function personalizedForStudent(username: string): Promise<PersonalizationTarget | undefined> {
const student = await getStudentRepository().findByUsername(username);
if (student) {
return {
type: 'student',
student: student,
};
}
return undefined;
}
/**
* Shortcut function to easily create a PersonalizationTarget object for a group by class name, assignment number and
* group number.
* @param classId Id of the class in which this group was created
* @param assignmentNumber Number of the assignment for which this group was created
* @param groupNumber Number of the group for which we want to personalize the learning path.
*/
export async function personalizedForGroup(
classId: string,
assignmentNumber: number,
groupNumber: number
): Promise<PersonalizationTarget | undefined> {
const clazz = await getClassRepository().findById(classId);
if (!clazz) {
return undefined;
}
const group = await getGroupRepository().findOne({
assignment: {
within: clazz,
id: assignmentNumber,
},
groupNumber: groupNumber,
});
if (group) {
return {
type: 'group',
group: group,
};
}
return undefined;
}
/**
* Returns the last submission for the learning object associated with the given node and for the student or group
*/
export async function getLastSubmissionForCustomizationTarget(node: LearningPathNode, pathFor: PersonalizationTarget): Promise<Submission | null> {
export async function getLastSubmissionForGroup(node: LearningPathNode, pathFor: Group): Promise<Submission | null> {
const submissionRepo = getSubmissionRepository();
const learningObjectId: LearningObjectIdentifier = {
hruid: node.learningObjectHruid,
language: node.language,
version: node.version,
};
if (pathFor.type === 'group') {
return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor.group);
}
return await submissionRepo.findMostRecentSubmissionForStudent(learningObjectId, pathFor.student);
return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor);
}
/**

View file

@ -1,6 +1,6 @@
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import { PersonalizationTarget } from './learning-path-personalization-util.js';
import { Language } from '@dwengo-1/common/util/language';
import { Group } from "../../entities/assignments/group.entity";
/**
* Generic interface for a service which provides access to learning paths from a data source.
@ -9,10 +9,10 @@ export interface LearningPathProvider {
/**
* Fetch the learning paths with the given hruids from the data source.
*/
fetchLearningPaths(hruids: string[], language: Language, source: string, personalizedFor?: PersonalizationTarget): Promise<LearningPathResponse>;
fetchLearningPaths(hruids: string[], language: Language, source: string, personalizedFor?: Group): Promise<LearningPathResponse>;
/**
* Search learning paths in the data source using the given search string.
*/
searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]>;
searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]>;
}

View file

@ -1,9 +1,9 @@
import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js';
import databaseLearningPathProvider from './database-learning-path-provider.js';
import { envVars, getEnvVar } from '../../util/envVars.js';
import { PersonalizationTarget } from './learning-path-personalization-util.js';
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language';
import {Group} from "../../entities/assignments/group.entity";
const userContentPrefix = getEnvVar(envVars.UserContentPrefix);
const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider];
@ -23,7 +23,7 @@ const learningPathService = {
hruids: string[],
language: Language,
source: string,
personalizedFor?: PersonalizationTarget
personalizedFor?: Group
): Promise<LearningPathResponse> {
const userContentHruids = hruids.filter((hruid) => hruid.startsWith(userContentPrefix));
const nonUserContentHruids = hruids.filter((hruid) => !hruid.startsWith(userContentPrefix));
@ -48,7 +48,7 @@ const learningPathService = {
/**
* Search learning paths in the data source using the given search string.
*/
async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> {
async searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]> {
const providerResponses = await Promise.all(
allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor))
);