Merge branch 'dev' into feat/caching

This commit is contained in:
Tibo De Peuter 2025-05-15 22:50:45 +02:00
commit fd6691f6aa
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
39 changed files with 1054 additions and 592 deletions

View file

@ -62,6 +62,11 @@ export async function getAllSubmissionsHandler(req: Request, res: Response): Pro
// TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
const submitter = req.body.submitter;
const usernameSubmitter = req.body.submitter.username;
const group = req.body.group;
requireFields({ group, submitter, usernameSubmitter });
const submissionDTO = req.body as SubmissionDTO;
const submission = await createSubmission(submissionDTO);

View file

@ -7,7 +7,6 @@ import {
getJoinRequestsByClass,
getStudentsByTeacher,
getTeacher,
getTeacherQuestions,
updateClassJoinRequestStatus,
} from '../services/teachers.js';
import { requireFields } from './error-helper.js';
@ -70,16 +69,6 @@ export async function getTeacherStudentHandler(req: Request, res: Response): Pro
res.json({ students });
}
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
const full = req.query.full === 'true';
requireFields({ username });
const questions = await getTeacherQuestions(username, full);
res.json({ questions });
}
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classId;
requireFields({ classId });

View file

@ -2,7 +2,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { Language } from '@dwengo-1/common/util/language';
import { Teacher } from '../../entities/users/teacher.entity.js';
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
@ -32,11 +31,4 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
}
);
}
public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
return this.find(
{ admins: teacher },
{ populate: ['admins'] } // Make sure to load admin relations
);
}
}

View file

@ -26,6 +26,9 @@ export class Assignment {
@Property({ type: 'string' })
learningPathHruid!: string;
@Property({ type: 'datetime', nullable: true })
deadline?: Date;
@Enum({
items: () => Language,
})

View file

@ -20,6 +20,7 @@ export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
description: assignment.description,
learningPath: assignment.learningPathHruid,
language: assignment.learningPathLanguage,
deadline: assignment.deadline ?? new Date(),
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
};
}
@ -31,6 +32,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
description: assignmentData.description,
learningPathHruid: assignmentData.learningPath,
learningPathLanguage: languageMap[assignmentData.language],
deadline: assignmentData.deadline,
groups: [],
});
}

View file

@ -6,7 +6,6 @@ import {
getStudentJoinRequestHandler,
getTeacherClassHandler,
getTeacherHandler,
getTeacherQuestionHandler,
getTeacherStudentHandler,
updateStudentJoinRequestHandler,
} from '../controllers/teachers.js';
@ -27,8 +26,6 @@ router.get('/:username/classes', getTeacherClassHandler);
router.get('/:username/students', getTeacherStudentHandler);
router.get('/:username/questions', getTeacherQuestionHandler);
router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler);
router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler);

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 { 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<LearningPathNode, FilteredLearningObject>
): Promise<LearningObjectNode> {
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(node, personalizedFor) : null;
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningPathNode(node), personalizedFor) : null;
const transitions = node.transitions
.filter(
(trans) =>

View file

@ -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<LearningPath> {
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<LearningPathResponse> {
async fetchLearningPaths(hruids: string[], language: string, source: string, personalizedFor: Group): Promise<LearningPathResponse> {
if (hruids.length === 0) {
return {
success: false,
@ -32,17 +54,23 @@ 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<LearningPath[]> {
async searchLearningPaths(query: string, language: string, personalizedFor: Group): Promise<LearningPath[]> {
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
const params = { all: query, language };
const searchResults = await fetchRemote<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params });
if (searchResults) {
await Promise.all(searchResults?.map(async (it) => addProgressToLearningPath(it, personalizedFor)));
}
return searchResults ?? [];
},
};

View file

@ -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<Submission | null> {
export async function getLastSubmissionForGroup(learningObjectId: LearningObjectIdentifier, pathFor: Group): Promise<Submission | null> {
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);
}
/**

View file

@ -42,7 +42,7 @@ export async function fetchStudent(username: string): Promise<Student> {
const user = await studentRepository.findByUsername(username);
if (!user) {
throw new NotFoundException('Student with username not found');
throw new NotFoundException(`Student with username ${username} not found`);
}
return user;

View file

@ -1,12 +1,5 @@
import {
getClassJoinRequestRepository,
getClassRepository,
getLearningObjectRepository,
getQuestionRepository,
getTeacherRepository,
} from '../data/repositories.js';
import { getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js';
import { mapToClassDTO } from '../interfaces/class.js';
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
import { Teacher } from '../entities/users/teacher.entity.js';
import { fetchStudent } from './students.js';
@ -15,10 +8,6 @@ import { mapToStudentRequestDTO } from '../interfaces/student-request.js';
import { TeacherRepository } from '../data/users/teacher-repository.js';
import { ClassRepository } from '../data/classes/class-repository.js';
import { Class } from '../entities/classes/class.entity.js';
import { LearningObjectRepository } from '../data/content/learning-object-repository.js';
import { LearningObject } from '../entities/content/learning-object.entity.js';
import { QuestionRepository } from '../data/questions/question-repository.js';
import { Question } from '../entities/questions/question.entity.js';
import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js';
import { Student } from '../entities/users/student.entity.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
@ -26,7 +15,6 @@ import { addClassStudent, fetchClass, getClassStudentsDTO } from './classes.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
import { ConflictException } from '../exceptions/conflict-exception.js';
@ -119,28 +107,6 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro
return students.map((student) => student.username);
}
export async function getTeacherQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const teacher: Teacher = await fetchTeacher(username);
// Find all learning objects that this teacher manages
const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository();
const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher);
if (!learningObjects || learningObjects.length === 0) {
return [];
}
// Fetch all questions related to these learning objects
const questionRepository: QuestionRepository = getQuestionRepository();
const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects);
if (full) {
return questions.map(mapToQuestionDTO);
}
return questions.map(mapToQuestionDTOId);
}
export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> {
const classRepository: ClassRepository = getClassRepository();
const cls: Class | null = await classRepository.findById(classId);