Merge branch 'dev' into docs/swagger-autogen

This commit is contained in:
Tibo De Peuter 2025-03-13 21:59:00 +01:00
commit d020c68600
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
79 changed files with 2795 additions and 494 deletions

7
backend/config.js Normal file
View file

@ -0,0 +1,7 @@
// Can be placed in dotenv but found it redundant
// Import dotenv from "dotenv";
// Load .env file
// Dotenv.config();
export const DWENGO_API_BASE = 'https://dwengo.org/backend/api';
export const FALLBACK_LANG = 'nl';
export const FALLBACK_SEQ_NUM = 1;

View file

@ -1,4 +1,5 @@
import { EnvVars, getEnvVar } from './util/envvars.js';
import { Language } from './entities/content/language.js';
// API
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
@ -7,3 +8,5 @@ export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage);
// Logging
export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info';
export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102';
export const FALLBACK_SEQ_NUM = 1;

View file

@ -0,0 +1,76 @@
import { Request, Response } from 'express';
import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js';
import { AssignmentDTO } from '../interfaces/assignment.js';
// Typescript is annoy with with parameter forwarding from class.ts
interface AssignmentParams {
classid: string;
id: string;
}
export async function getAllAssignmentsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const classid = req.params.classid;
const full = req.query.full === 'true';
const assignments = await getAllAssignments(classid, full);
res.json({
assignments: assignments,
});
}
export async function createAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const classid = req.params.classid;
const assignmentData = req.body as AssignmentDTO;
if (!assignmentData.description || !assignmentData.language || !assignmentData.learningPath || !assignmentData.title) {
res.status(400).json({
error: 'Missing one or more required fields: title, description, learningPath, language',
});
return;
}
const assignment = await createAssignment(classid, assignmentData);
if (!assignment) {
res.status(500).json({ error: 'Could not create assignment ' });
return;
}
res.status(201).json({ assignment: assignment });
}
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const id = +req.params.id;
const classid = req.params.classid;
if (isNaN(id)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const assignment = await getAssignment(classid, id);
if (!assignment) {
res.status(404).json({ error: 'Assignment not found' });
return;
}
res.json(assignment);
}
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const classid = req.params.classid;
const assignmentNumber = +req.params.id;
if (isNaN(assignmentNumber)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber);
res.json({
submissions: submissions,
});
}

View file

@ -0,0 +1,77 @@
import { Request, Response } from 'express';
import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/class.js';
import { ClassDTO } from '../interfaces/class.js';
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const classes = await getAllClasses(full);
res.json({
classes: classes,
});
}
export async function createClassHandler(req: Request, res: Response): Promise<void> {
const classData = req.body as ClassDTO;
if (!classData.displayName) {
res.status(400).json({
error: 'Missing one or more required fields: displayName',
});
return;
}
const cls = await createClass(classData);
if (!cls) {
res.status(500).json({ error: 'Something went wrong while creating class' });
return;
}
res.status(201).json({ class: cls });
}
export async function getClassHandler(req: Request, res: Response): Promise<void> {
try {
const classId = req.params.id;
const cls = await getClass(classId);
if (!cls) {
res.status(404).json({ error: 'Class not found' });
return;
}
cls.endpoints = {
self: `${req.baseUrl}/${req.params.id}`,
invitations: `${req.baseUrl}/${req.params.id}/invitations`,
assignments: `${req.baseUrl}/${req.params.id}/assignments`,
students: `${req.baseUrl}/${req.params.id}/students`,
};
res.json(cls);
} catch (error) {
console.error('Error fetching learning objects:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const full = req.query.full === 'true';
const students = full ? await getClassStudents(classId) : await getClassStudentsIds(classId);
res.json({
students: students,
});
}
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const full = req.query.full === 'true'; // TODO: not implemented yet
const invitations = await getClassTeacherInvitations(classId, full);
res.json({
invitations: invitations,
});
}

View file

@ -0,0 +1,95 @@
import { Request, Response } from 'express';
import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js';
import { GroupDTO } from '../interfaces/group.js';
// Typescript is annoywith with parameter forwarding from class.ts
interface GroupParams {
classid: string;
assignmentid: string;
groupid?: string;
}
export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> {
const classId = req.params.classid;
const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid;
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const groupId = +req.params.groupid!; // Can't be undefined
if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' });
return;
}
const group = await getGroup(classId, assignmentId, groupId, full);
res.json(group);
}
export async function getAllGroupsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid;
const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid;
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const groups = await getAllGroups(classId, assignmentId, full);
res.json({
groups: groups,
});
}
export async function createGroupHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid;
const assignmentId = +req.params.assignmentid;
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const groupData = req.body as GroupDTO;
const group = await createGroup(groupData, classid, assignmentId);
if (!group) {
res.status(500).json({ error: 'Something went wrong while creating group' });
return;
}
res.status(201).json({ group: group });
}
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid;
// Const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid;
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const groupId = +req.params.groupid!; // Can't be undefined
if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' });
return;
}
const submissions = await getGroupSubmissions(classId, assignmentId, groupId);
res.json({
submissions: submissions,
});
}

View file

@ -1,45 +0,0 @@
import { Request, Response } from 'express';
import { themes } from '../data/themes.js';
import { FALLBACK_LANG } from '../config.js';
import { getLogger } from '../logging/initalize.js';
import learningPathService from '../services/learning-paths/learning-path-service.js';
import { Language } from '../entities/content/language.js';
/**
* Fetch learning paths based on query parameters.
*/
export async function getLearningPaths(req: Request, res: Response): Promise<void> {
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 Language) || FALLBACK_LANG;
let hruidList;
if (hruids) {
hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)];
} else if (themeKey) {
const theme = themes.find((t) => 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) => theme.hruids);
}
const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language, `HRUIDs: ${hruidList.join(', ')}`);
res.json(learningPaths.data);
} catch (error) {
getLogger().error('❌ Unexpected error fetching learning paths:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

View file

@ -0,0 +1,119 @@
import { Request, Response } from 'express';
import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js';
import { QuestionDTO, QuestionId } from '../interfaces/question.js';
import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { Language } from '../entities/content/language.js';
function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null {
const { hruid, version } = req.params;
const lang = req.query.lang;
if (!hruid || !version) {
res.status(400).json({ error: 'Missing required parameters.' });
return null;
}
return {
hruid,
language: (lang as Language) || FALLBACK_LANG,
version: +version,
};
}
function getQuestionId(req: Request, res: Response): QuestionId | null {
const seq = req.params.seq;
const learningObjectIdentifier = getObjectId(req, res);
if (!learningObjectIdentifier) {
return null;
}
return {
learningObjectIdentifier,
sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM,
};
}
export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> {
const objectId = getObjectId(req, res);
const full = req.query.full === 'true';
if (!objectId) {
return;
}
const questions = await getAllQuestions(objectId, full);
if (!questions) {
res.status(404).json({ error: `Questions not found.` });
} else {
res.json(questions);
}
}
export async function getQuestionHandler(req: Request, res: Response): Promise<void> {
const questionId = getQuestionId(req, res);
if (!questionId) {
return;
}
const question = await getQuestion(questionId);
if (!question) {
res.status(404).json({ error: `Question not found.` });
} else {
res.json(question);
}
}
export async function getQuestionAnswersHandler(req: Request, res: Response): Promise<void> {
const questionId = getQuestionId(req, res);
const full = req.query.full === 'true';
if (!questionId) {
return;
}
const answers = getAnswersByQuestion(questionId, full);
if (!answers) {
res.status(404).json({ error: `Questions not found.` });
} else {
res.json(answers);
}
}
export async function createQuestionHandler(req: Request, res: Response): Promise<void> {
const questionDTO = req.body as QuestionDTO;
if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) {
res.status(400).json({ error: 'Missing required fields: identifier and content' });
return;
}
const question = await createQuestion(questionDTO);
if (!question) {
res.status(400).json({ error: 'Could not add question' });
} else {
res.json(question);
}
}
export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> {
const questionId = getQuestionId(req, res);
if (!questionId) {
return;
}
const question = await deleteQuestion(questionId);
if (!question) {
res.status(400).json({ error: 'Could not find nor delete question' });
} else {
res.json(question);
}
}

View file

@ -0,0 +1,146 @@
import { Request, Response } from 'express';
import {
createStudent,
deleteStudent,
getAllStudents,
getStudent,
getStudentAssignments,
getStudentClasses,
getStudentGroups,
getStudentSubmissions,
} from '../services/students.js';
import { ClassDTO } from '../interfaces/class.js';
import { getAllAssignments } from '../services/assignments.js';
import { getUserHandler } from './users.js';
import { Student } from '../entities/users/student.entity.js';
import { StudentDTO } from '../interfaces/student.js';
import { getStudentRepository } from '../data/repositories.js';
import { UserDTO } from '../interfaces/user.js';
// TODO: accept arguments (full, ...)
// TODO: endpoints
export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const studentRepository = getStudentRepository();
const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents();
if (!students) {
res.status(404).json({ error: `Student not found.` });
return;
}
res.status(201).json(students);
}
export async function getStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const user = await getStudent(username);
if (!user) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(201).json(user);
}
export async function createStudentHandler(req: Request, res: Response) {
const userData = req.body as StudentDTO;
if (!userData.username || !userData.firstName || !userData.lastName) {
res.status(400).json({
error: 'Missing required fields: username, firstName, lastName',
});
return;
}
const newUser = await createStudent(userData);
res.status(201).json(newUser);
}
export async function deleteStudentHandler(req: Request, res: Response) {
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const deletedUser = await deleteStudent(username);
if (!deletedUser) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(200).json(deletedUser);
}
export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> {
try {
const full = req.query.full === 'true';
const username = req.params.id;
const classes = await getStudentClasses(username, full);
res.json({
classes: classes,
endpoints: {
self: `${req.baseUrl}/${req.params.id}`,
classes: `${req.baseUrl}/${req.params.id}/invitations`,
questions: `${req.baseUrl}/${req.params.id}/assignments`,
students: `${req.baseUrl}/${req.params.id}/students`,
},
});
} catch (error) {
console.error('Error fetching learning objects:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
// TODO
// Might not be fully correct depending on if
// A class has an assignment, that all students
// Have this assignment.
export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const username = req.params.id;
const assignments = getStudentAssignments(username, full);
res.json({
assignments: assignments,
});
}
export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const username = req.params.id;
const groups = await getStudentGroups(username, full);
res.json({
groups: groups,
});
}
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> {
const username = req.params.id;
const submissions = await getStudentSubmissions(username);
res.json({
submissions: submissions,
});
}

View file

@ -0,0 +1,59 @@
import { Request, Response } from 'express';
import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js';
import { Language, languageMap } from '../entities/content/language.js';
import { SubmissionDTO } from '../interfaces/submission';
interface SubmissionParams {
hruid: string;
id: number;
}
export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> {
const lohruid = req.params.hruid;
const submissionNumber = +req.params.id;
if (isNaN(submissionNumber)) {
res.status(400).json({ error: 'Submission number is not a number' });
return;
}
const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = (req.query.version || 1) as number;
const submission = await getSubmission(lohruid, lang, version, submissionNumber);
if (!submission) {
res.status(404).json({ error: 'Submission not found' });
return;
}
res.json(submission);
}
export async function createSubmissionHandler(req: Request, res: Response) {
const submissionDTO = req.body as SubmissionDTO;
const submission = await createSubmission(submissionDTO);
if (!submission) {
res.status(404).json({ error: 'Submission not added' });
} else {
res.json(submission);
}
}
export async function deleteSubmissionHandler(req: Request, res: Response) {
const hruid = req.params.hruid;
const submissionNumber = +req.params.id;
const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = (req.query.version || 1) as number;
const submission = await deleteSubmission(hruid, lang, version, submissionNumber);
if (!submission) {
res.status(404).json({ error: 'Submission not found' });
} else {
res.json(submission);
}
}

View file

@ -0,0 +1,144 @@
import { Request, Response } from 'express';
import {
createTeacher,
deleteTeacher,
getAllTeachers,
getClassesByTeacher,
getClassIdsByTeacher,
getQuestionIdsByTeacher,
getQuestionsByTeacher,
getStudentIdsByTeacher,
getStudentsByTeacher,
getTeacher,
} from '../services/teachers.js';
import { ClassDTO } from '../interfaces/class.js';
import { StudentDTO } from '../interfaces/student.js';
import { QuestionDTO, QuestionId } from '../interfaces/question.js';
import { Teacher } from '../entities/users/teacher.entity.js';
import { TeacherDTO } from '../interfaces/teacher.js';
import { getTeacherRepository } from '../data/repositories.js';
export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const teacherRepository = getTeacherRepository();
const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers();
if (!teachers) {
res.status(404).json({ error: `Teacher not found.` });
return;
}
res.status(201).json(teachers);
}
export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const user = await getTeacher(username);
if (!user) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(201).json(user);
}
export async function createTeacherHandler(req: Request, res: Response) {
const userData = req.body as TeacherDTO;
if (!userData.username || !userData.firstName || !userData.lastName) {
res.status(400).json({
error: 'Missing required fields: username, firstName, lastName',
});
return;
}
const newUser = await createTeacher(userData);
res.status(201).json(newUser);
}
export async function deleteTeacherHandler(req: Request, res: Response) {
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const deletedUser = await deleteTeacher(username);
if (!deletedUser) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(200).json(deletedUser);
}
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
try {
const username = req.params.username as string;
const full = req.query.full === 'true';
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const classes: ClassDTO[] | string[] = full ? await getClassesByTeacher(username) : await getClassIdsByTeacher(username);
res.status(201).json(classes);
} catch (error) {
console.error('Error fetching classes by teacher:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
try {
const username = req.params.username as string;
const full = req.query.full === 'true';
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const students: StudentDTO[] | string[] = full ? await getStudentsByTeacher(username) : await getStudentIdsByTeacher(username);
res.status(201).json(students);
} catch (error) {
console.error('Error fetching students by teacher:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
try {
const username = req.params.username as string;
const full = req.query.full === 'true';
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const questions: QuestionDTO[] | QuestionId[] = full ? await getQuestionsByTeacher(username) : await getQuestionIdsByTeacher(username);
res.status(201).json(questions);
} catch (error) {
console.error('Error fetching questions by teacher:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

View file

@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import { themes } from '../data/themes.js';
import { loadTranslations } from '../util/translationHelper.js';
import { loadTranslations } from '../util/translation-helper.js';
interface Translations {
curricula_page: {

View file

@ -0,0 +1,91 @@
import { Request, Response } from 'express';
import { UserService } from '../services/users.js';
import { UserDTO } from '../interfaces/user.js';
import { User } from '../entities/users/user.entity.js';
export async function getAllUsersHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> {
try {
const full = req.query.full === 'true';
const users: UserDTO[] | string[] = full ? await service.getAllUsers() : await service.getAllUserIds();
if (!users) {
res.status(404).json({ error: `Users not found.` });
return;
}
res.status(201).json(users);
} catch (error) {
console.error('❌ Error fetching users:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
export async function getUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> {
try {
const username = req.params.username as string;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const user = await service.getUserByUsername(username);
if (!user) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(201).json(user);
} catch (error) {
console.error('❌ Error fetching users:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
export async function createUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>, UserClass: new () => T) {
try {
console.log('req', req);
const userData = req.body as UserDTO;
if (!userData.username || !userData.firstName || !userData.lastName) {
res.status(400).json({
error: 'Missing required fields: username, firstName, lastName',
});
return;
}
const newUser = await service.createUser(userData, UserClass);
res.status(201).json(newUser);
} catch (error) {
console.error('❌ Error creating user:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
export async function deleteUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>) {
try {
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const deletedUser = await service.deleteUser(username);
if (!deletedUser) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(200).json(deletedUser);
} catch (error) {
console.error('❌ Error deleting user:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

View file

@ -1,16 +1,26 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Group } from '../../entities/assignments/group.entity.js';
import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Student } from '../../entities/users/student.entity.js';
export class GroupRepository extends DwengoEntityRepository<Group> {
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
return this.findOne({
return this.findOne(
{
assignment: assignment,
groupNumber: groupNumber,
});
},
{ populate: ['members'] }
);
}
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
return this.findAll({ where: { assignment: assignment } });
return this.findAll({
where: { assignment: assignment },
populate: ['members'],
});
}
public findAllGroupsWithStudent(student: Student): Promise<Group[]> {
return this.find({ members: student }, { populate: ['members'] });
}
public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) {
return this.deleteWhere({

View file

@ -38,6 +38,14 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
);
}
public findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
return this.find({ onBehalfOf: group });
}
public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
return this.find({ submitter: student });
}
public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
return this.deleteWhere({
learningObjectHruid: loId.hruid,

View file

@ -1,11 +1,23 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Class } from '../../entities/classes/class.entity.js';
import { Student } from '../../entities/users/student.entity.js';
import { Teacher } from '../../entities/users/teacher.entity';
export class ClassRepository extends DwengoEntityRepository<Class> {
public findById(id: string): Promise<Class | null> {
return this.findOne({ classId: id });
return this.findOne({ classId: id }, { populate: ['students', 'teachers'] });
}
public deleteById(id: string): Promise<void> {
return this.deleteWhere({ classId: id });
}
public findByStudent(student: Student): Promise<Class[]> {
return this.find(
{ students: student },
{ populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
);
}
public findByTeacher(teacher: Teacher): Promise<Class[]> {
return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] });
}
}

View file

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

View file

@ -2,6 +2,7 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Question } from '../../entities/questions/question.entity.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { Student } from '../../entities/users/student.entity.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js';
export class QuestionRepository extends DwengoEntityRepository<Question> {
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
@ -40,4 +41,17 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
sequenceNumber: sequenceNumber,
});
}
public async findAllByLearningObjects(learningObjects: LearningObject[]): Promise<Question[]> {
const objectIdentifiers = learningObjects.map((lo) => ({
learningObjectHruid: lo.hruid,
learningObjectLanguage: lo.language,
learningObjectVersion: lo.version,
}));
return this.findAll({
where: { $or: objectIdentifiers },
orderBy: { timestamp: 'ASC' },
});
}
}

View file

@ -54,7 +54,6 @@ function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(en
}
/* Users */
export const getUserRepository = repositoryGetter<User, UserRepository>(User);
export const getStudentRepository = repositoryGetter<Student, StudentRepository>(Student);
export const getTeacherRepository = repositoryGetter<Teacher, TeacherRepository>(Teacher);

View file

@ -1,5 +1,9 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Student } from '../../entities/users/student.entity.js';
import { User } from '../../entities/users/user.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
// Import { UserRepository } from './user-repository.js';
// Export class StudentRepository extends UserRepository<Student> {}
export class StudentRepository extends DwengoEntityRepository<Student> {
public findByUsername(username: string): Promise<Student | null> {

View file

@ -1,5 +1,6 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Teacher } from '../../entities/users/teacher.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { UserRepository } from './user-repository.js';
export class TeacherRepository extends DwengoEntityRepository<Teacher> {
public findByUsername(username: string): Promise<Teacher | null> {

View file

@ -1,11 +1,11 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { User } from '../../entities/users/user.entity.js';
export class UserRepository extends DwengoEntityRepository<User> {
public findByUsername(username: string): Promise<User | null> {
return this.findOne({ username: username });
export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
public findByUsername(username: string): Promise<T | null> {
return this.findOne({ username } as Partial<T>);
}
public deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username: username });
return this.deleteWhere({ username } as Partial<T>);
}
}

View file

@ -1,16 +1,21 @@
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Class } from '../classes/class.entity.js';
import { Group } from './group.entity.js';
import { Language } from '../content/language.js';
import { AssignmentRepository } from '../../data/assignments/assignment-repository.js';
@Entity({ repository: () => AssignmentRepository })
@Entity({
repository: () => AssignmentRepository,
})
export class Assignment {
@ManyToOne({ entity: () => Class, primary: true })
@ManyToOne({
entity: () => Class,
primary: true,
})
within!: Class;
@PrimaryKey({ type: 'number' })
id!: number;
@PrimaryKey({ type: 'number', autoincrement: true })
id?: number;
@Property({ type: 'string' })
title!: string;
@ -21,9 +26,14 @@ export class Assignment {
@Property({ type: 'string' })
learningPathHruid!: string;
@Enum({ items: () => Language })
@Enum({
items: () => Language,
})
learningPathLanguage!: Language;
@OneToMany({ entity: () => Group, mappedBy: 'assignment' })
@OneToMany({
entity: () => Group,
mappedBy: 'assignment',
})
groups!: Group[];
}

View file

@ -1,9 +1,11 @@
import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
import { Collection, Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
import { Assignment } from './assignment.entity.js';
import { Student } from '../users/student.entity.js';
import { GroupRepository } from '../../data/assignments/group-repository.js';
@Entity({ repository: () => GroupRepository })
@Entity({
repository: () => GroupRepository,
})
export class Group {
@ManyToOne({
entity: () => Assignment,
@ -11,8 +13,8 @@ export class Group {
})
assignment!: Assignment;
@PrimaryKey({ type: 'integer' })
groupNumber!: number;
@PrimaryKey({ type: 'integer', autoincrement: true })
groupNumber?: number;
@ManyToMany({
entity: () => Student,

View file

@ -18,7 +18,7 @@ export class Submission {
@PrimaryKey({ type: 'numeric' })
learningObjectVersion: number = 1;
@PrimaryKey({ type: 'integer' })
@PrimaryKey({ type: 'integer', autoincrement: true })
submissionNumber!: number;
@ManyToOne({

View file

@ -3,7 +3,9 @@ import { Student } from '../users/student.entity.js';
import { Class } from './class.entity.js';
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
@Entity({ repository: () => ClassJoinRequestRepository })
@Entity({
repository: () => ClassJoinRequestRepository,
})
export class ClassJoinRequest {
@ManyToOne({
entity: () => Student,

View file

@ -4,10 +4,12 @@ import { Teacher } from '../users/teacher.entity.js';
import { Student } from '../users/student.entity.js';
import { ClassRepository } from '../../data/classes/class-repository.js';
@Entity({ repository: () => ClassRepository })
@Entity({
repository: () => ClassRepository,
})
export class Class {
@PrimaryKey()
classId = v4();
classId? = v4();
@Property({ type: 'string' })
displayName!: string;

View file

@ -7,9 +7,6 @@ import { TeacherInvitationRepository } from '../../data/classes/teacher-invitati
* Invitation of a teacher into a class (in order to teach it).
*/
@Entity({ repository: () => TeacherInvitationRepository })
@Entity({
repository: () => TeacherInvitationRepository,
})
export class TeacherInvitation {
@ManyToOne({
entity: () => Teacher,

View file

@ -2,7 +2,9 @@ import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
import { LearningObject } from './learning-object.entity.js';
import { AttachmentRepository } from '../../data/content/attachment-repository.js';
@Entity({ repository: () => AttachmentRepository })
@Entity({
repository: () => AttachmentRepository,
})
export class Attachment {
@ManyToOne({
entity: () => LearningObject,

View file

@ -184,3 +184,10 @@ export enum Language {
Zhuang = 'za',
Zulu = 'zu',
}
export const languageMap: Record<string, Language> = {
nl: Language.Dutch,
fr: Language.French,
en: Language.English,
de: Language.German,
};

View file

@ -0,0 +1,38 @@
import { mapToUserDTO, UserDTO } from './user.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js';
import { Answer } from '../entities/questions/answer.entity.js';
export interface AnswerDTO {
author: UserDTO;
toQuestion: QuestionDTO;
sequenceNumber: number;
timestamp: string;
content: string;
}
/**
* Convert a Question entity to a DTO format.
*/
export function mapToAnswerDTO(answer: Answer): AnswerDTO {
return {
author: mapToUserDTO(answer.author),
toQuestion: mapToQuestionDTO(answer.toQuestion),
sequenceNumber: answer.sequenceNumber!,
timestamp: answer.timestamp.toISOString(),
content: answer.content,
};
}
export interface AnswerId {
author: string;
toQuestion: QuestionId;
sequenceNumber: number;
}
export function mapToAnswerId(answer: AnswerDTO): AnswerId {
return {
author: answer.author.username,
toQuestion: mapToQuestionId(answer.toQuestion),
sequenceNumber: answer.sequenceNumber,
};
}

View file

@ -0,0 +1,52 @@
import { FALLBACK_LANG } from '../config.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { Class } from '../entities/classes/class.entity.js';
import { languageMap } from '../entities/content/language.js';
import { GroupDTO, mapToGroupDTO } from './group.js';
export interface AssignmentDTO {
id: number;
class: string; // Id of class 'within'
title: string;
description: string;
learningPath: string;
language: string;
groups?: GroupDTO[] | string[]; // TODO
}
export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO {
return {
id: assignment.id!,
class: assignment.within.classId!,
title: assignment.title,
description: assignment.description,
learningPath: assignment.learningPathHruid,
language: assignment.learningPathLanguage,
// Groups: assignment.groups.map(group => group.groupNumber),
};
}
export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
return {
id: assignment.id!,
class: assignment.within.classId!,
title: assignment.title,
description: assignment.description,
learningPath: assignment.learningPathHruid,
language: assignment.learningPathLanguage,
// Groups: assignment.groups.map(mapToGroupDTO),
};
}
export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment {
const assignment = new Assignment();
assignment.title = assignmentData.title;
assignment.description = assignmentData.description;
assignment.learningPathHruid = assignmentData.learningPath;
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
assignment.within = cls;
console.log(assignment);
return assignment;
}

View file

@ -0,0 +1,37 @@
import { Collection } from '@mikro-orm/core';
import { Class } from '../entities/classes/class.entity.js';
import { Student } from '../entities/users/student.entity.js';
import { Teacher } from '../entities/users/teacher.entity.js';
export interface ClassDTO {
id: string;
displayName: string;
teachers: string[];
students: string[];
joinRequests: string[];
endpoints?: {
self: string;
invitations: string;
assignments: string;
students: string;
};
}
export function mapToClassDTO(cls: Class): ClassDTO {
return {
id: cls.classId!,
displayName: cls.displayName,
teachers: cls.teachers.map((teacher) => teacher.username),
students: cls.students.map((student) => student.username),
joinRequests: [], // TODO
};
}
export function mapToClass(classData: ClassDTO, students: Collection<Student>, teachers: Collection<Teacher>): Class {
const cls = new Class();
cls.displayName = classData.displayName;
cls.students = students;
cls.teachers = teachers;
return cls;
}

View file

@ -0,0 +1,25 @@
import { Group } from '../entities/assignments/group.entity.js';
import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js';
import { mapToStudentDTO, StudentDTO } from './student.js';
export interface GroupDTO {
assignment: number | AssignmentDTO;
groupNumber: number;
members: string[] | StudentDTO[];
}
export function mapToGroupDTO(group: Group): GroupDTO {
return {
assignment: mapToAssignmentDTO(group.assignment), // ERROR: , group.assignment.within),
groupNumber: group.groupNumber!,
members: group.members.map(mapToStudentDTO),
};
}
export function mapToGroupDTOId(group: Group): GroupDTO {
return {
assignment: group.assignment.id!,
groupNumber: group.groupNumber!,
members: group.members.map((member) => member.username),
};
}

View file

@ -0,0 +1,5 @@
// TODO: implement something like this but with named endpoints
export interface List<T> {
items: T[];
endpoints?: string[];
}

View file

@ -0,0 +1,44 @@
import { Question } from '../entities/questions/question.entity.js';
import { UserDTO } from './user.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToStudentDTO, StudentDTO } from './student.js';
import { TeacherDTO } from './teacher.js';
export interface QuestionDTO {
learningObjectIdentifier: LearningObjectIdentifier;
sequenceNumber?: number;
author: StudentDTO;
timestamp?: string;
content: string;
}
/**
* Convert a Question entity to a DTO format.
*/
export function mapToQuestionDTO(question: Question): QuestionDTO {
const learningObjectIdentifier = {
hruid: question.learningObjectHruid,
language: question.learningObjectLanguage,
version: question.learningObjectVersion,
};
return {
learningObjectIdentifier,
sequenceNumber: question.sequenceNumber!,
author: mapToStudentDTO(question.author),
timestamp: question.timestamp.toISOString(),
content: question.content,
};
}
export interface QuestionId {
learningObjectIdentifier: LearningObjectIdentifier;
sequenceNumber: number;
}
export function mapToQuestionId(question: QuestionDTO): QuestionId {
return {
learningObjectIdentifier: question.learningObjectIdentifier,
sequenceNumber: question.sequenceNumber!,
};
}

View file

@ -0,0 +1,29 @@
import { Student } from '../entities/users/student.entity.js';
export interface StudentDTO {
id: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
classes: string;
questions: string;
invitations: string;
groups: string;
};
}
export function mapToStudentDTO(student: Student): StudentDTO {
return {
id: student.username,
username: student.username,
firstName: student.firstName,
lastName: student.lastName,
};
}
export function mapToStudent(studentData: StudentDTO): Student {
const student = new Student(studentData.username, studentData.firstName, studentData.lastName);
return student;
}

View file

@ -0,0 +1,47 @@
import { Submission } from '../entities/assignments/submission.entity.js';
import { Language } from '../entities/content/language.js';
import { GroupDTO, mapToGroupDTO } from './group.js';
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js';
import { mapToUser } from './user';
import { Student } from '../entities/users/student.entity';
export interface SubmissionDTO {
learningObjectHruid: string;
learningObjectLanguage: Language;
learningObjectVersion: number;
submissionNumber?: number;
submitter: StudentDTO;
time?: Date;
group?: GroupDTO;
content: string;
}
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
return {
learningObjectHruid: submission.learningObjectHruid,
learningObjectLanguage: submission.learningObjectLanguage,
learningObjectVersion: submission.learningObjectVersion,
submissionNumber: submission.submissionNumber,
submitter: mapToStudentDTO(submission.submitter),
time: submission.submissionTime,
group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf) : undefined,
content: submission.content,
};
}
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
const submission = new Submission();
submission.learningObjectHruid = submissionDTO.learningObjectHruid;
submission.learningObjectLanguage = submissionDTO.learningObjectLanguage;
submission.learningObjectVersion = submissionDTO.learningObjectVersion;
// Submission.submissionNumber = submissionDTO.submissionNumber;
submission.submitter = mapToStudent(submissionDTO.submitter);
// Submission.submissionTime = submissionDTO.time;
// Submission.onBehalfOf = submissionDTO.group!;
// TODO fix group
submission.content = submissionDTO.content;
return submission;
}

View file

@ -0,0 +1,25 @@
import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js';
import { ClassDTO, mapToClassDTO } from './class.js';
import { mapToUserDTO, UserDTO } from './user.js';
export interface TeacherInvitationDTO {
sender: string | UserDTO;
receiver: string | UserDTO;
class: string | ClassDTO;
}
export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO {
return {
sender: mapToUserDTO(invitation.sender),
receiver: mapToUserDTO(invitation.receiver),
class: mapToClassDTO(invitation.class),
};
}
export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): TeacherInvitationDTO {
return {
sender: invitation.sender.username,
receiver: invitation.receiver.username,
class: invitation.class.classId!,
};
}

View file

@ -0,0 +1,29 @@
import { Teacher } from '../entities/users/teacher.entity.js';
export interface TeacherDTO {
id: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
classes: string;
questions: string;
invitations: string;
groups: string;
};
}
export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
return {
id: teacher.username,
username: teacher.username,
firstName: teacher.firstName,
lastName: teacher.lastName,
};
}
export function mapToTeacher(TeacherData: TeacherDTO): Teacher {
const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName);
return teacher;
}

View file

@ -0,0 +1,30 @@
import { User } from '../entities/users/user.entity.js';
export interface UserDTO {
id?: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
self: string;
classes: string;
questions: string;
invitations: string;
};
}
export function mapToUserDTO(user: User): UserDTO {
return {
id: user.username,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
};
}
export function mapToUser<T extends User>(userData: UserDTO, userInstance: T): T {
userInstance.username = userData.username;
userInstance.firstName = userData.firstName;
userInstance.lastName = userData.lastName;
return userInstance;
}

View file

@ -1,45 +0,0 @@
import express from 'express';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
assignments: ['0', '1'],
});
});
// Information about an assignment with id 'id'
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
title: 'Dit is een test assignment',
description: 'Een korte beschrijving',
groups: ['0'],
learningPath: '0',
class: '0',
links: {
self: `${req.baseUrl}/${req.params.id}`,
submissions: `${req.baseUrl}/${req.params.id}`,
},
});
});
router.get('/:id/submissions', (req, res) => {
res.json({
submissions: ['0'],
});
});
router.get('/:id/groups', (req, res) => {
res.json({
groups: ['0'],
});
});
router.get('/:id/questions', (req, res) => {
res.json({
questions: ['0'],
});
});
export default router;

View file

@ -0,0 +1,30 @@
import express from 'express';
import {
createAssignmentHandler,
getAllAssignmentsHandler,
getAssignmentHandler,
getAssignmentsSubmissionsHandler,
} from '../controllers/assignments.js';
import groupRouter from './groups.js';
const router = express.Router({ mergeParams: true });
// Root endpoint used to search objects
router.get('/', getAllAssignmentsHandler);
router.post('/', createAssignmentHandler);
// Information about an assignment with id 'id'
router.get('/:id', getAssignmentHandler);
router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
router.get('/:id/questions', (req, res) => {
res.json({
questions: ['0'],
});
});
router.use('/:assignmentid/groups', groupRouter);
export default router;

View file

@ -1,46 +0,0 @@
import express from 'express';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
classes: ['0', '1'],
});
});
// Information about an class with id 'id'
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
displayName: 'Klas 4B',
teachers: ['0'],
students: ['0'],
joinRequests: ['0'],
links: {
self: `${req.baseUrl}/${req.params.id}`,
classes: `${req.baseUrl}/${req.params.id}/invitations`,
questions: `${req.baseUrl}/${req.params.id}/assignments`,
students: `${req.baseUrl}/${req.params.id}/students`,
},
});
});
router.get('/:id/invitations', (req, res) => {
res.json({
invitations: ['0'],
});
});
router.get('/:id/assignments', (req, res) => {
res.json({
assignments: ['0'],
});
});
router.get('/:id/students', (req, res) => {
res.json({
students: ['0'],
});
});
export default router;

View file

@ -0,0 +1,27 @@
import express from 'express';
import {
createClassHandler,
getAllClassesHandler,
getClassHandler,
getClassStudentsHandler,
getTeacherInvitationsHandler,
} from '../controllers/classes.js';
import assignmentRouter from './assignments.js';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', getAllClassesHandler);
router.post('/', createClassHandler);
// Information about an class with id 'id'
router.get('/:id', getClassHandler);
router.get('/:id/teacher-invitations', getTeacherInvitationsHandler);
router.get('/:id/students', getClassStudentsHandler);
router.use('/:classid/assignments', assignmentRouter);
export default router;

View file

@ -1,31 +0,0 @@
import express from 'express';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
groups: ['0', '1'],
});
});
// Information about a group (members, ... [TODO DOC])
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
assignment: '0',
students: ['0'],
submissions: ['0'],
// Reference to other endpoint
// Should be less hardcoded
questions: `/group/${req.params.id}/question`,
});
});
// The list of questions a group has made
router.get('/:id/question', (req, res) => {
res.json({
questions: ['0'],
});
});
export default router;

View file

@ -0,0 +1,23 @@
import express from 'express';
import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js';
const router = express.Router({ mergeParams: true });
// Root endpoint used to search objects
router.get('/', getAllGroupsHandler);
router.post('/', createGroupHandler);
// Information about a group (members, ... [TODO DOC])
router.get('/:groupid', getGroupHandler);
router.get('/:groupid', getGroupSubmissionsHandler);
// The list of questions a group has made
router.get('/:id/questions', (req, res) => {
res.json({
questions: ['0'],
});
});
export default router;

View file

@ -1,6 +1,9 @@
import express from 'express';
import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js';
import submissionRoutes from './submissions.js';
import questionRoutes from './questions.js';
const router = express.Router();
// DWENGO learning objects
@ -21,6 +24,10 @@ router.get('/', getAllLearningObjects);
// Example: http://localhost:3000/learningObject/un_ai7
router.get('/:hruid', getLearningObject);
router.use('/:hruid/submissions', submissionRoutes);
router.use('/:hruid/:version/questions', questionRoutes);
// Parameter: hruid of learning object
// Query: language, version (optional)
// Route to fetch the HTML rendering of one learning object based on its hruid.

View file

@ -1,33 +0,0 @@
import express from 'express';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
questions: ['0', '1'],
});
});
// Information about an question with id 'id'
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
student: '0',
group: '0',
time: new Date(2025, 1, 1),
content: 'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????',
learningObject: '0',
links: {
self: `${req.baseUrl}/${req.params.id}`,
answers: `${req.baseUrl}/${req.params.id}/answers`,
},
});
});
router.get('/:id/answers', (req, res) => {
res.json({
answers: ['0'],
});
});
export default router;

View file

@ -0,0 +1,25 @@
import express from 'express';
import {
createQuestionHandler,
deleteQuestionHandler,
getAllQuestionsHandler,
getQuestionAnswersHandler,
getQuestionHandler,
} from '../controllers/questions.js';
const router = express.Router({ mergeParams: true });
// Query language
// Root endpoint used to search objects
router.get('/', getAllQuestionsHandler);
router.post('/', createQuestionHandler);
router.delete('/:seq', deleteQuestionHandler);
// Information about a question with id
router.get('/:seq', getQuestionHandler);
router.get('/answers/:seq', getQuestionAnswersHandler);
export default router;

View file

@ -1,55 +0,0 @@
import express from 'express';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
students: ['0', '1'],
});
});
// Information about a student's profile
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
firstName: 'Jimmy',
lastName: 'Faster',
username: 'JimmyFaster2',
endpoints: {
classes: `/student/${req.params.id}/classes`,
questions: `/student/${req.params.id}/submissions`,
invitations: `/student/${req.params.id}/assignments`,
groups: `/student/${req.params.id}/groups`,
},
});
});
// The list of classes a student is in
router.get('/:id/classes', (req, res) => {
res.json({
classes: ['0'],
});
});
// The list of submissions a student has made
router.get('/:id/submissions', (req, res) => {
res.json({
submissions: ['0'],
});
});
// The list of assignments a student has
router.get('/:id/assignments', (req, res) => {
res.json({
assignments: ['0'],
});
});
// The list of groups a student is in
router.get('/:id/groups', (req, res) => {
res.json({
groups: ['0'],
});
});
export default router;

View file

@ -0,0 +1,46 @@
import express from 'express';
import {
createStudentHandler,
deleteStudentHandler,
getAllStudentsHandler,
getStudentAssignmentsHandler,
getStudentClassesHandler,
getStudentGroupsHandler,
getStudentHandler,
getStudentSubmissionsHandler,
} from '../controllers/students.js';
import { getStudentGroups } from '../services/students.js';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', getAllStudentsHandler);
router.post('/', createStudentHandler);
router.delete('/', deleteStudentHandler);
router.delete('/:username', deleteStudentHandler);
// Information about a student's profile
router.get('/:username', getStudentHandler);
// The list of classes a student is in
router.get('/:id/classes', getStudentClassesHandler);
// The list of submissions a student has made
router.get('/:id/submissions', getStudentSubmissionsHandler);
// The list of assignments a student has
router.get('/:id/assignments', getStudentAssignmentsHandler);
// The list of groups a student is in
router.get('/:id/groups', getStudentGroupsHandler);
// A list of questions a user has created
router.get('/:id/questions', (req, res) => {
res.json({
questions: ['0'],
});
});
export default router;

View file

@ -1,23 +0,0 @@
import express from 'express';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
submissions: ['0', '1'],
});
});
// Information about an submission with id 'id'
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
student: '0',
group: '0',
time: new Date(2025, 1, 1),
content: 'Wortel 2 is rationeel',
learningObject: '0',
});
});
export default router;

View file

@ -0,0 +1,19 @@
import express from 'express';
import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler } from '../controllers/submissions.js';
const router = express.Router({ mergeParams: true });
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
submissions: ['0', '1'],
});
});
router.post('/:id', createSubmissionHandler);
// Information about an submission with id 'id'
router.get('/:id', getSubmissionHandler);
router.delete('/:id', deleteSubmissionHandler);
export default router;

View file

@ -1,48 +0,0 @@
import express from 'express';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
teachers: ['0', '1'],
});
});
// Information about a teacher
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
firstName: 'John',
lastName: 'Doe',
username: 'JohnDoe1',
links: {
self: `${req.baseUrl}/${req.params.id}`,
classes: `${req.baseUrl}/${req.params.id}/classes`,
questions: `${req.baseUrl}/${req.params.id}/questions`,
invitations: `${req.baseUrl}/${req.params.id}/invitations`,
},
});
});
// The questions students asked a teacher
router.get('/:id/questions', (req, res) => {
res.json({
questions: ['0'],
});
});
// Invitations to other classes a teacher received
router.get('/:id/invitations', (req, res) => {
res.json({
invitations: ['0'],
});
});
// A list with ids of classes a teacher is in
router.get('/:id/classes', (req, res) => {
res.json({
classes: ['0'],
});
});
export default router;

View file

@ -0,0 +1,37 @@
import express from 'express';
import {
createTeacherHandler,
deleteTeacherHandler,
getAllTeachersHandler,
getTeacherClassHandler,
getTeacherHandler,
getTeacherQuestionHandler,
getTeacherStudentHandler,
} from '../controllers/teachers.js';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', getAllTeachersHandler);
router.post('/', createTeacherHandler);
router.delete('/', deleteTeacherHandler);
router.get('/:username', getTeacherHandler);
router.delete('/:username', deleteTeacherHandler);
router.get('/:username/classes', getTeacherClassHandler);
router.get('/:username/students', getTeacherStudentHandler);
router.get('/:username/questions', getTeacherQuestionHandler);
// Invitations to other classes a teacher received
router.get('/:id/invitations', (req, res) => {
res.json({
invitations: ['0'],
});
});
export default router;

View file

@ -0,0 +1,85 @@
import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
if (!cls) {
return [];
}
const assignmentRepository = getAssignmentRepository();
const assignments = await assignmentRepository.findAllAssignmentsInClass(cls);
if (full) {
return assignments.map(mapToAssignmentDTO);
}
return assignments.map(mapToAssignmentDTOId);
}
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<Assignment | null> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
if (!cls) {
return null;
}
const assignment = mapToAssignment(assignmentData, cls);
const assignmentRepository = getAssignmentRepository();
try {
const newAssignment = assignmentRepository.create(assignment);
await assignmentRepository.save(newAssignment);
return newAssignment;
} catch (e) {
return null;
}
}
export async function getAssignment(classid: string, id: number): Promise<AssignmentDTO | null> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
if (!cls) {
return null;
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, id);
if (!assignment) {
return null;
}
return mapToAssignmentDTO(assignment);
}
export async function getAssignmentsSubmissions(classid: string, assignmentNumber: number): Promise<SubmissionDTO[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
if (!cls) {
return [];
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
return [];
}
const groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
const submissionRepository = getSubmissionRepository();
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
return submissions.map(mapToSubmissionDTO);
}

View file

@ -0,0 +1,99 @@
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js';
import { Class } from '../entities/classes/class.entity.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js';
import { getLogger } from '../logging/initalize';
const logger = getLogger();
export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[]> {
const classRepository = getClassRepository();
const classes = await classRepository.find({}, { populate: ['students', 'teachers'] });
if (!classes) {
return [];
}
if (full) {
return classes.map(mapToClassDTO);
}
return classes.map((cls) => cls.classId!);
}
export async function createClass(classData: ClassDTO): Promise<Class | null> {
const teacherRepository = getTeacherRepository();
const teacherUsernames = classData.teachers || [];
const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher != null);
const studentRepository = getStudentRepository();
const studentUsernames = classData.students || [];
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null);
//Const cls = mapToClass(classData, teachers, students);
const classRepository = getClassRepository();
try {
const newClass = classRepository.create({
displayName: classData.displayName,
teachers: teachers,
students: students,
});
await classRepository.save(newClass);
return newClass;
} catch (e) {
logger.error(e);
return null;
}
}
export async function getClass(classId: string): Promise<ClassDTO | null> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
if (!cls) {
return null;
}
return mapToClassDTO(cls);
}
async function fetchClassStudents(classId: string): Promise<StudentDTO[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
if (!cls) {
return [];
}
return cls.students.map(mapToStudentDTO);
}
export async function getClassStudents(classId: string): Promise<StudentDTO[]> {
return await fetchClassStudents(classId);
}
export async function getClassStudentsIds(classId: string): Promise<string[]> {
const students: StudentDTO[] = await fetchClassStudents(classId);
return students.map((student) => student.username);
}
export async function getClassTeacherInvitations(classId: string, full: boolean): Promise<TeacherInvitationDTO[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
if (!cls) {
return [];
}
const teacherInvitationRepository = getTeacherInvitationRepository();
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(cls);
if (full) {
return invitations.map(mapToTeacherInvitationDTO);
}
return invitations.map(mapToTeacherInvitationDTOIds);
}

View file

@ -0,0 +1,132 @@
import { GroupRepository } from '../data/assignments/group-repository.js';
import {
getAssignmentRepository,
getClassRepository,
getGroupRepository,
getStudentRepository,
getSubmissionRepository,
} from '../data/repositories.js';
import { Group } from '../entities/assignments/group.entity.js';
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
if (!cls) {
return null;
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
return null;
}
const groupRepository = getGroupRepository();
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber);
if (!group) {
return null;
}
if (full) {
return mapToGroupDTO(group);
}
return mapToGroupDTOId(group);
}
export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise<Group | null> {
const studentRepository = getStudentRepository();
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null);
console.log(members);
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
if (!cls) {
return null;
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
return null;
}
const groupRepository = getGroupRepository();
try {
const newGroup = groupRepository.create({
assignment: assignment,
members: members,
});
await groupRepository.save(newGroup);
return newGroup;
} catch (e) {
console.log(e);
return null;
}
}
export async function getAllGroups(classId: string, assignmentNumber: number, full: boolean): Promise<GroupDTO[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
if (!cls) {
return [];
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
return [];
}
const groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
if (full) {
console.log('full');
console.log(groups);
return groups.map(mapToGroupDTO);
}
return groups.map(mapToGroupDTOId);
}
export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise<SubmissionDTO[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
if (!cls) {
return [];
}
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
return [];
}
const groupRepository = getGroupRepository();
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber);
if (!group) {
return [];
}
const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findAllSubmissionsForGroup(group);
return submissions.map(mapToSubmissionDTO);
}

View file

@ -0,0 +1,90 @@
import { DWENGO_API_BASE } from '../config.js';
import { fetchWithLogging } from '../util/api-helper.js';
import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js';
function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
return {
key: data.hruid, // Hruid learningObject (not path)
_id: data._id,
uuid: data.uuid,
version: data.version,
title: data.title,
htmlUrl, // Url to fetch html content
language: data.language,
difficulty: data.difficulty,
estimatedTime: data.estimated_time,
available: data.available,
teacherExclusive: data.teacher_exclusive,
educationalGoals: data.educational_goals, // List with learningObjects
keywords: data.keywords, // For search
description: data.description, // For search (not an actual description)
targetAges: data.target_ages,
contentType: data.content_type, // Markdown, image, audio, etc.
contentLocation: data.content_location, // If content type extern
skosConcepts: data.skos_concepts,
returnValue: data.return_value, // Callback response information
};
}
/**
* Fetches a single learning object by its HRUID
*/
export async function getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> {
const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`;
const metadata = await fetchWithLogging<LearningObjectMetadata>(
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)
*/
async function fetchLearningObjects(hruid: string, full: boolean, language: string): Promise<FilteredLearningObject[] | string[]> {
try {
const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`);
if (!learningPathResponse.success || !learningPathResponse.data?.length) {
console.error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`);
return [];
}
const nodes: LearningObjectNode[] = learningPathResponse.data[0].nodes;
if (!full) {
return nodes.map((node) => node.learningobject_hruid);
}
return await Promise.all(nodes.map(async (node) => getLearningObjectById(node.learningobject_hruid, language))).then((objects) =>
objects.filter((obj): obj is FilteredLearningObject => obj !== null)
);
} catch (error) {
console.error('❌ Error fetching learning objects:', error);
return [];
}
}
/**
* Fetch full learning object data (metadata)
*/
export async function getLearningObjectsFromPath(hruid: string, language: string): Promise<FilteredLearningObject[]> {
return (await fetchLearningObjects(hruid, true, language)) as FilteredLearningObject[];
}
/**
* Fetch only learning object HRUIDs
*/
export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> {
return (await fetchLearningObjects(hruid, false, language)) as string[];
}
function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
throw new Error('Function not implemented.');
}

View file

@ -1,5 +1,5 @@
import { DWENGO_API_BASE } from '../../config.js';
import { fetchWithLogging } from '../../util/apiHelper.js';
import { fetchWithLogging } from '../../util/api-helper.js';
import {
FilteredLearningObject,
LearningObjectIdentifier,

View file

@ -29,7 +29,7 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
)
)
);
if (nullableNodesToLearningObjects.values().some((it) => it === null)) {
if (Array.from(nullableNodesToLearningObjects.values()).some((it) => it === null)) {
throw new Error('At least one of the learning objects on this path could not be found.');
}
return nullableNodesToLearningObjects as Map<LearningPathNode, FilteredLearningObject>;
@ -41,15 +41,9 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: PersonalizationTarget): Promise<LearningPath> {
const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes);
const targetAges = nodesToLearningObjects
.values()
.flatMap((it) => it.targetAges || [])
.toArray();
const targetAges = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.targetAges || []);
const keywords = nodesToLearningObjects
.values()
.flatMap((it) => it.keywords || [])
.toArray();
const keywords = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.keywords || []);
const image = learningPath.image ? learningPath.image.toString('base64') : undefined;
@ -83,9 +77,7 @@ async function convertNodes(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget
): Promise<LearningObjectNode[]> {
const nodesPromise = nodesToLearningObjects
.entries()
.map(async (entry) => {
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => {
const [node, learningObject] = entry;
const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null;
return {
@ -102,8 +94,7 @@ async function convertNodes(
)
.map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition
};
})
.toArray();
});
return await Promise.all(nodesPromise);
}

View file

@ -1,4 +1,4 @@
import { fetchWithLogging } from '../../util/apiHelper.js';
import { fetchWithLogging } from '../../util/api-helper.js';
import { DWENGO_API_BASE } from '../../config.js';
import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js';
import { LearningPathProvider } from './learning-path-provider.js';

View file

@ -0,0 +1,107 @@
import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
import { Question } from '../entities/questions/question.entity.js';
import { Answer } from '../entities/questions/answer.entity.js';
import { mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js';
import { QuestionRepository } from '../data/questions/question-repository.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToUser } from '../interfaces/user.js';
import { Student } from '../entities/users/student.entity.js';
import { mapToStudent } from '../interfaces/student.js';
export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const questionRepository: QuestionRepository = getQuestionRepository();
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
if (!questions) {
return [];
}
const questionsDTO: QuestionDTO[] = questions.map(mapToQuestionDTO);
if (full) {
return questionsDTO;
}
return questionsDTO.map(mapToQuestionId);
}
async function fetchQuestion(questionId: QuestionId): Promise<Question | null> {
const questionRepository = getQuestionRepository();
return await questionRepository.findOne({
learningObjectHruid: questionId.learningObjectIdentifier.hruid,
learningObjectLanguage: questionId.learningObjectIdentifier.language,
learningObjectVersion: questionId.learningObjectIdentifier.version,
sequenceNumber: questionId.sequenceNumber,
});
}
export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO | null> {
const question = await fetchQuestion(questionId);
if (!question) {
return null;
}
return mapToQuestionDTO(question);
}
export async function getAnswersByQuestion(questionId: QuestionId, full: boolean) {
const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId);
if (!question) {
return [];
}
const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question);
if (!answers) {
return [];
}
const answersDTO = answers.map(mapToAnswerDTO);
if (full) {
return answersDTO;
}
return answersDTO.map(mapToAnswerId);
}
export async function createQuestion(questionDTO: QuestionDTO) {
const questionRepository = getQuestionRepository();
const author = mapToStudent(questionDTO.author);
try {
await questionRepository.createQuestion({
loId: questionDTO.learningObjectIdentifier,
author,
content: questionDTO.content,
});
} catch (e) {
return null;
}
return questionDTO;
}
export async function deleteQuestion(questionId: QuestionId) {
const questionRepository = getQuestionRepository();
const question = await fetchQuestion(questionId);
if (!question) {
return null;
}
try {
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(questionId.learningObjectIdentifier, questionId.sequenceNumber);
} catch (e) {
return null;
}
return question;
}

View file

@ -0,0 +1,126 @@
import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js';
import { Class } from '../entities/classes/class.entity.js';
import { Student } from '../entities/users/student.entity.js';
import { AssignmentDTO } from '../interfaces/assignment.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
import { getAllAssignments } from './assignments.js';
import { UserService } from './users.js';
export async function getAllStudents(): Promise<StudentDTO[]> {
const studentRepository = getStudentRepository();
const users = await studentRepository.findAll();
return users.map(mapToStudentDTO);
}
export async function getAllStudentIds(): Promise<string[]> {
const users = await getAllStudents();
return users.map((user) => user.username);
}
export async function getStudent(username: string): Promise<StudentDTO | null> {
const studentRepository = getStudentRepository();
const user = await studentRepository.findByUsername(username);
return user ? mapToStudentDTO(user) : null;
}
export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> {
const studentRepository = getStudentRepository();
try {
const newStudent = studentRepository.create(mapToStudent(userData));
await studentRepository.save(newStudent);
return mapToStudentDTO(newStudent);
} catch (e) {
console.log(e);
return null;
}
}
export async function deleteStudent(username: string): Promise<StudentDTO | null> {
const studentRepository = getStudentRepository();
const user = await studentRepository.findByUsername(username);
if (!user) {
return null;
}
try {
await studentRepository.deleteByUsername(username);
return mapToStudentDTO(user);
} catch (e) {
console.log(e);
return null;
}
}
export async function getStudentClasses(username: string, full: boolean): Promise<ClassDTO[] | string[]> {
const studentRepository = getStudentRepository();
const student = await studentRepository.findByUsername(username);
if (!student) {
return [];
}
const classRepository = getClassRepository();
const classes = await classRepository.findByStudent(student);
if (full) {
return classes.map(mapToClassDTO);
}
return classes.map((cls) => cls.classId!);
}
export async function getStudentAssignments(username: string, full: boolean): Promise<AssignmentDTO[]> {
const studentRepository = getStudentRepository();
const student = await studentRepository.findByUsername(username);
if (!student) {
return [];
}
const classRepository = getClassRepository();
const classes = await classRepository.findByStudent(student);
const assignments = (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat();
return assignments;
}
export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[]> {
const studentRepository = getStudentRepository();
const student = await studentRepository.findByUsername(username);
if (!student) {
return [];
}
const groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsWithStudent(student);
if (full) {
return groups.map(mapToGroupDTO);
}
return groups.map(mapToGroupDTOId);
}
export async function getStudentSubmissions(username: string): Promise<SubmissionDTO[]> {
const studentRepository = getStudentRepository();
const student = await studentRepository.findByUsername(username);
if (!student) {
return [];
}
const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findAllSubmissionsForStudent(student);
return submissions.map(mapToSubmissionDTO);
}

View file

@ -0,0 +1,51 @@
import { getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { Language } from '../entities/content/language.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
export async function getSubmission(
learningObjectHruid: string,
language: Language,
version: number,
submissionNumber: number
): Promise<SubmissionDTO | null> {
const loId = new LearningObjectIdentifier(learningObjectHruid, language, version);
const submissionRepository = getSubmissionRepository();
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber);
if (!submission) {
return null;
}
return mapToSubmissionDTO(submission);
}
export async function createSubmission(submissionDTO: SubmissionDTO) {
const submissionRepository = getSubmissionRepository();
const submission = mapToSubmission(submissionDTO);
try {
const newSubmission = await submissionRepository.create(submission);
await submissionRepository.save(newSubmission);
} catch (e) {
return null;
}
return submission;
}
export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) {
const submissionRepository = getSubmissionRepository();
const submission = getSubmission(learningObjectHruid, language, version, submissionNumber);
if (!submission) {
return null;
}
const loId = new LearningObjectIdentifier(learningObjectHruid, language, version);
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber);
return submission;
}

View file

@ -0,0 +1,129 @@
import {
getClassRepository,
getLearningObjectRepository,
getQuestionRepository,
getStudentRepository,
getTeacherRepository,
} from '../data/repositories.js';
import { Teacher } from '../entities/users/teacher.entity.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { getClassStudents } from './class.js';
import { StudentDTO } from '../interfaces/student.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
import { UserService } from './users.js';
import { mapToUser } from '../interfaces/user.js';
import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js';
export async function getAllTeachers(): Promise<TeacherDTO[]> {
const teacherRepository = getTeacherRepository();
const users = await teacherRepository.findAll();
return users.map(mapToTeacherDTO);
}
export async function getAllTeacherIds(): Promise<string[]> {
const users = await getAllTeachers();
return users.map((user) => user.username);
}
export async function getTeacher(username: string): Promise<TeacherDTO | null> {
const teacherRepository = getTeacherRepository();
const user = await teacherRepository.findByUsername(username);
return user ? mapToTeacherDTO(user) : null;
}
export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> {
const teacherRepository = getTeacherRepository();
try {
const newTeacher = teacherRepository.create(mapToTeacher(userData));
await teacherRepository.save(newTeacher);
return mapToTeacherDTO(newTeacher);
} catch (e) {
console.log(e);
return null;
}
}
export async function deleteTeacher(username: string): Promise<TeacherDTO | null> {
const teacherRepository = getTeacherRepository();
const user = await teacherRepository.findByUsername(username);
if (!user) {
return null;
}
try {
await teacherRepository.deleteByUsername(username);
return mapToTeacherDTO(user);
} catch (e) {
console.log(e);
return null;
}
}
export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[]> {
const teacherRepository = getTeacherRepository();
const teacher = await teacherRepository.findByUsername(username);
if (!teacher) {
return [];
}
const classRepository = getClassRepository();
const classes = await classRepository.findByTeacher(teacher);
return classes.map(mapToClassDTO);
}
export async function getClassesByTeacher(username: string): Promise<ClassDTO[]> {
return await fetchClassesByTeacher(username);
}
export async function getClassIdsByTeacher(username: string): Promise<string[]> {
const classes = await fetchClassesByTeacher(username);
return classes.map((cls) => cls.id);
}
export async function fetchStudentsByTeacher(username: string) {
const classes = await getClassIdsByTeacher(username);
return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat();
}
export async function getStudentsByTeacher(username: string): Promise<StudentDTO[]> {
return await fetchStudentsByTeacher(username);
}
export async function getStudentIdsByTeacher(username: string): Promise<string[]> {
const students = await fetchStudentsByTeacher(username);
return students.map((student) => student.username);
}
export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[]> {
const teacherRepository = getTeacherRepository();
const teacher = await teacherRepository.findByUsername(username);
if (!teacher) {
throw new Error(`Teacher with username '${username}' not found.`);
}
// Find all learning objects that this teacher manages
const learningObjectRepository = getLearningObjectRepository();
const learningObjects = await learningObjectRepository.findAllByTeacher(teacher);
// Fetch all questions related to these learning objects
const questionRepository = getQuestionRepository();
const questions = await questionRepository.findAllByLearningObjects(learningObjects);
return questions.map(mapToQuestionDTO);
}
export async function getQuestionsByTeacher(username: string): Promise<QuestionDTO[]> {
return await fetchTeacherQuestions(username);
}
export async function getQuestionIdsByTeacher(username: string): Promise<QuestionId[]> {
const questions = await fetchTeacherQuestions(username);
return questions.map(mapToQuestionId);
}

View file

@ -0,0 +1,41 @@
import { UserRepository } from '../data/users/user-repository.js';
import { UserDTO, mapToUser, mapToUserDTO } from '../interfaces/user.js';
import { User } from '../entities/users/user.entity.js';
export class UserService<T extends User> {
protected repository: UserRepository<T>;
constructor(repository: UserRepository<T>) {
this.repository = repository;
}
async getAllUsers(): Promise<UserDTO[]> {
const users = await this.repository.findAll();
return users.map(mapToUserDTO);
}
async getAllUserIds(): Promise<string[]> {
const users = await this.getAllUsers();
return users.map((user) => user.username);
}
async getUserByUsername(username: string): Promise<UserDTO | null> {
const user = await this.repository.findByUsername(username);
return user ? mapToUserDTO(user) : null;
}
async createUser(userData: UserDTO, UserClass: new () => T): Promise<T> {
const newUser = mapToUser(userData, new UserClass());
await this.repository.save(newUser);
return newUser;
}
async deleteUser(username: string): Promise<UserDTO | null> {
const user = await this.getUserByUsername(username);
if (!user) {
return null;
}
await this.repository.deleteByUsername(username);
return mapToUserDTO(user);
}
}

View file

@ -32,7 +32,7 @@ describe('SubmissionRepository', () => {
});
it('should find the requested submission', async () => {
const id = new LearningObjectIdentifier('id03', Language.English, '1');
const id = new LearningObjectIdentifier('id03', Language.English, 1);
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);
expect(submission).toBeTruthy();
@ -40,7 +40,7 @@ describe('SubmissionRepository', () => {
});
it('should find the most recent submission for a student', async () => {
const id = new LearningObjectIdentifier('id02', Language.English, '1');
const id = new LearningObjectIdentifier('id02', Language.English, 1);
const student = await studentRepository.findByUsername('Noordkaap');
const submission = await submissionRepository.findMostRecentSubmissionForStudent(id, student!);
@ -49,7 +49,7 @@ describe('SubmissionRepository', () => {
});
it('should find the most recent submission for a group', async () => {
const id = new LearningObjectIdentifier('id03', Language.English, '1');
const id = new LearningObjectIdentifier('id03', Language.English, 1);
const class_ = await classRepository.findById('id01');
const assignment = await assignmentRepository.findByClassAndId(class_!, 1);
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1);
@ -60,7 +60,7 @@ describe('SubmissionRepository', () => {
});
it('should not find a deleted submission', async () => {
const id = new LearningObjectIdentifier('id01', Language.English, '1');
const id = new LearningObjectIdentifier('id01', Language.English, 1);
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1);
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);

View file

@ -17,11 +17,11 @@ describe('AttachmentRepository', () => {
});
it('should return the requested attachment', async () => {
const id = new LearningObjectIdentifier('id02', Language.English, '1');
const id = new LearningObjectIdentifier('id02', Language.English, 1);
const learningObject = await learningObjectRepository.findByIdentifier(id);
const attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName(
learningObject!,
learningObject!.hruid,
Language.English,
'attachment01'
);

View file

@ -13,8 +13,8 @@ describe('LearningObjectRepository', () => {
learningObjectRepository = getLearningObjectRepository();
});
const id01 = new LearningObjectIdentifier('id01', Language.English, '1');
const id02 = new LearningObjectIdentifier('test_id', Language.English, '1');
const id01 = new LearningObjectIdentifier('id01', Language.English, 1);
const id02 = new LearningObjectIdentifier('test_id', Language.English, 1);
it('should return the learning object that matches identifier 1', async () => {
const learningObject = await learningObjectRepository.findByIdentifier(id01);

View file

@ -20,7 +20,7 @@ describe('AnswerRepository', () => {
});
it('should find all answers to a question', async () => {
const id = new LearningObjectIdentifier('id05', Language.English, '1');
const id = new LearningObjectIdentifier('id05', Language.English, 1);
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
const question = questions.filter((it) => it.sequenceNumber == 2)[0];
@ -35,7 +35,7 @@ describe('AnswerRepository', () => {
it('should create an answer to a question', async () => {
const teacher = await teacherRepository.findByUsername('FooFighters');
const id = new LearningObjectIdentifier('id05', Language.English, '1');
const id = new LearningObjectIdentifier('id05', Language.English, 1);
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
const question = questions[0];
@ -54,7 +54,7 @@ describe('AnswerRepository', () => {
});
it('should not find a removed answer', async () => {
const id = new LearningObjectIdentifier('id04', Language.English, '1');
const id = new LearningObjectIdentifier('id04', Language.English, 1);
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
await answerRepository.removeAnswerByQuestionAndSequenceNumber(questions[0], 1);

View file

@ -20,7 +20,7 @@ describe('QuestionRepository', () => {
});
it('should return all questions part of the given learning object', async () => {
const id = new LearningObjectIdentifier('id05', Language.English, '1');
const id = new LearningObjectIdentifier('id05', Language.English, 1);
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
expect(questions).toBeTruthy();
@ -28,7 +28,7 @@ describe('QuestionRepository', () => {
});
it('should create new question', async () => {
const id = new LearningObjectIdentifier('id03', Language.English, '1');
const id = new LearningObjectIdentifier('id03', Language.English, 1);
const student = await studentRepository.findByUsername('Noordkaap');
await questionRepository.createQuestion({
loId: id,
@ -42,7 +42,7 @@ describe('QuestionRepository', () => {
});
it('should not find removed question', async () => {
const id = new LearningObjectIdentifier('id04', Language.English, '1');
const id = new LearningObjectIdentifier('id04', Language.English, 1);
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(id, 1);
const question = await questionRepository.findAllQuestionsAboutLearningObject(id);

View file

@ -12,7 +12,7 @@ export function makeTestSubmissions(
const submission01 = em.create(Submission, {
learningObjectHruid: 'id03',
learningObjectLanguage: Language.English,
learningObjectVersion: '1',
learningObjectVersion: 1,
submissionNumber: 1,
submitter: students[0],
submissionTime: new Date(2025, 2, 20),
@ -23,7 +23,7 @@ export function makeTestSubmissions(
const submission02 = em.create(Submission, {
learningObjectHruid: 'id03',
learningObjectLanguage: Language.English,
learningObjectVersion: '1',
learningObjectVersion: 1,
submissionNumber: 2,
submitter: students[0],
submissionTime: new Date(2025, 2, 25),
@ -34,7 +34,7 @@ export function makeTestSubmissions(
const submission03 = em.create(Submission, {
learningObjectHruid: 'id02',
learningObjectLanguage: Language.English,
learningObjectVersion: '1',
learningObjectVersion: 1,
submissionNumber: 1,
submitter: students[0],
submissionTime: new Date(2025, 2, 20),
@ -44,7 +44,7 @@ export function makeTestSubmissions(
const submission04 = em.create(Submission, {
learningObjectHruid: 'id02',
learningObjectLanguage: Language.English,
learningObjectVersion: '1',
learningObjectVersion: 1,
submissionNumber: 2,
submitter: students[0],
submissionTime: new Date(2025, 2, 25),
@ -54,7 +54,7 @@ export function makeTestSubmissions(
const submission05 = em.create(Submission, {
learningObjectHruid: 'id01',
learningObjectLanguage: Language.English,
learningObjectVersion: '1',
learningObjectVersion: 1,
submissionNumber: 1,
submitter: students[1],
submissionTime: new Date(2025, 2, 20),

View file

@ -77,7 +77,7 @@ export function makeTestLearningPaths(em: EntityManager<IDatabaseDriver<Connecti
admins: [],
title: 'repertoire Tool',
description: 'all about Tool',
image: '',
image: null,
nodes: nodes01,
});
@ -92,7 +92,7 @@ export function makeTestLearningPaths(em: EntityManager<IDatabaseDriver<Connecti
admins: [],
title: 'repertoire Dire Straits',
description: 'all about Dire Straits',
image: '',
image: null,
nodes: nodes02,
});

View file

@ -6,7 +6,7 @@ import { Student } from '../../../src/entities/users/student.entity';
export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>, students: Array<Student>): Array<Question> {
const question01 = em.create(Question, {
learningObjectLanguage: Language.English,
learningObjectVersion: '1',
learningObjectVersion: 1,
learningObjectHruid: 'id05',
sequenceNumber: 1,
author: students[0],
@ -16,7 +16,7 @@ export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>
const question02 = em.create(Question, {
learningObjectLanguage: Language.English,
learningObjectVersion: '1',
learningObjectVersion: 1,
learningObjectHruid: 'id05',
sequenceNumber: 2,
author: students[2],
@ -26,7 +26,7 @@ export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>
const question03 = em.create(Question, {
learningObjectLanguage: Language.English,
learningObjectVersion: '1',
learningObjectVersion: 1,
learningObjectHruid: 'id04',
sequenceNumber: 1,
author: students[0],
@ -36,7 +36,7 @@ export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>
const question04 = em.create(Question, {
learningObjectLanguage: Language.English,
learningObjectVersion: '1',
learningObjectVersion: 1,
learningObjectHruid: 'id01',
sequenceNumber: 1,
author: students[1],

460
package-lock.json generated

File diff suppressed because it is too large Load diff