Merge branch 'dev' into github-actions/coverage

This commit is contained in:
Timo De Meyst 2025-04-14 11:00:56 +02:00
commit bd266d3594
111 changed files with 3698 additions and 2908 deletions

21
backend/.env-old Normal file
View file

@ -0,0 +1,21 @@
PORT=3000
DWENGO_DB_HOST=db
DWENGO_DB_PORT=5432
DWENGO_DB_USERNAME=postgres
DWENGO_DB_PASSWORD=postgres
DWENGO_DB_UPDATE=false
DWENGO_AUTH_STUDENT_URL=http://localhost/idp/realms/student
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs
DWENGO_AUTH_TEACHER_URL=http://localhost/idp/realms/teacher
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs
# Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production!
#DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/,127.0.0.1:80,http://127.0.0.1,http://localhost:80,http://127.0.0.1:80,localhost
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080
# Logging and monitoring
LOKI_HOST=http://logging:3102

View file

@ -6,8 +6,9 @@ WORKDIR /app/dwengo
COPY package*.json ./ COPY package*.json ./
COPY backend/package.json ./backend/ COPY backend/package.json ./backend/
# Backend depends on common # Backend depends on common and docs
COPY common/package.json ./common/ COPY common/package.json ./common/
COPY docs/package.json ./docs/
RUN npm install --silent RUN npm install --silent
@ -34,6 +35,7 @@ COPY ./backend/i18n ./i18n
COPY --from=build-stage /app/dwengo/common/dist ./common/dist COPY --from=build-stage /app/dwengo/common/dist ./common/dist
COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist
COPY --from=build-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json
COPY package*.json ./ COPY package*.json ./
COPY backend/package.json ./backend/ COPY backend/package.json ./backend/
@ -42,9 +44,7 @@ COPY common/package.json ./common/
RUN npm install --silent --only=production RUN npm install --silent --only=production
COPY ./docs ./docs
COPY ./backend/i18n ./backend/i18n COPY ./backend/i18n ./backend/i18n
COPY ./backend/.env ./backend/.env
EXPOSE 3000 EXPOSE 3000

View file

@ -7,7 +7,7 @@
"main": "dist/app.js", "main": "dist/app.js",
"scripts": { "scripts": {
"build": "cross-env NODE_ENV=production tsc --build", "build": "cross-env NODE_ENV=production tsc --build",
"dev": "cross-env NODE_ENV=development tsx watch --env-file=.env.development.local src/app.ts", "dev": "cross-env NODE_ENV=development tsx tool/seed.ts; tsx watch --env-file=.env.development.local src/app.ts",
"start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js", "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js",
"format": "prettier --write src/", "format": "prettier --write src/",
"format-check": "prettier --check src/", "format-check": "prettier --check src/",

View file

@ -5,3 +5,4 @@ export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl);
export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage); export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage);
export const FALLBACK_SEQ_NUM = 1; export const FALLBACK_SEQ_NUM = 1;
export const FALLBACK_VERSION_NUM = 1;

View file

@ -0,0 +1,99 @@
import { Request, Response } from 'express';
import { requireFields } from './error-helper.js';
import { getLearningObjectId, getQuestionId } from './questions.js';
import { createAnswer, deleteAnswer, getAnswer, getAnswersByQuestion, updateAnswer } from '../services/answers.js';
import { FALLBACK_SEQ_NUM } from '../config.js';
import { AnswerData } from '@dwengo-1/common/interfaces/answer';
export async function getAllAnswersHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid;
const version = req.params.version;
const language = req.query.lang as string;
const seq = req.params.seq;
const full = req.query.full === 'true';
requireFields({ hruid });
const learningObjectId = getLearningObjectId(hruid, version, language);
const questionId = getQuestionId(learningObjectId, seq);
const answers = await getAnswersByQuestion(questionId, full);
res.json({ answers });
}
export async function getAnswerHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid;
const version = req.params.version;
const language = req.query.lang as string;
const seq = req.params.seq;
const seqAnswer = req.params.seqAnswer;
requireFields({ hruid });
const learningObjectId = getLearningObjectId(hruid, version, language);
const questionId = getQuestionId(learningObjectId, seq);
const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM;
const answer = await getAnswer(questionId, sequenceNumber);
res.json({ answer });
}
export async function createAnswerHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid;
const version = req.params.version;
const language = req.query.lang as string;
const seq = req.params.seq;
requireFields({ hruid });
const learningObjectId = getLearningObjectId(hruid, version, language);
const questionId = getQuestionId(learningObjectId, seq);
const author = req.body.author as string;
const content = req.body.content as string;
requireFields({ author, content });
const answerData = req.body as AnswerData;
const answer = await createAnswer(questionId, answerData);
res.json({ answer });
}
export async function deleteAnswerHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid;
const version = req.params.version;
const language = req.query.lang as string;
const seq = req.params.seq;
const seqAnswer = req.params.seqAnswer;
requireFields({ hruid });
const learningObjectId = getLearningObjectId(hruid, version, language);
const questionId = getQuestionId(learningObjectId, seq);
const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM;
const answer = await deleteAnswer(questionId, sequenceNumber);
res.json({ answer });
}
export async function updateAnswerHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid;
const version = req.params.version;
const language = req.query.lang as string;
const seq = req.params.seq;
const seqAnswer = req.params.seqAnswer;
requireFields({ hruid });
const learningObjectId = getLearningObjectId(hruid, version, language);
const questionId = getQuestionId(learningObjectId, seq);
const content = req.body.content as string;
requireFields({ content });
const answerData = req.body as AnswerData;
const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM;
const answer = await updateAnswer(questionId, sequenceNumber, answerData);
res.json({ answer });
}

View file

@ -1,77 +1,94 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; import {
createAssignment,
deleteAssignment,
getAllAssignments,
getAssignment,
getAssignmentsSubmissions,
putAssignment,
} from '../services/assignments.js';
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
import { requireFields } from './error-helper.js';
import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { EntityDTO } from '@mikro-orm/core';
// Typescript is annoying with parameter forwarding from class.ts export async function getAllAssignmentsHandler(req: Request, res: Response): Promise<void> {
interface AssignmentParams { const classId = req.params.classid;
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 full = req.query.full === 'true';
const assignments = await getAllAssignments(classid, full); const assignments = await getAllAssignments(classId, full);
res.json({ res.json({ assignments });
assignments: assignments,
});
} }
export async function createAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { export async function createAssignmentHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid; const classid = req.params.classid;
const description = req.body.description;
const language = req.body.language;
const learningPath = req.body.learningPath;
const title = req.body.title;
requireFields({ description, language, learningPath, title });
const assignmentData = req.body as AssignmentDTO; 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); const assignment = await createAssignment(classid, assignmentData);
if (!assignment) { res.json({ assignment });
res.status(500).json({ error: 'Could not create assignment ' });
return;
}
res.status(201).json(assignment);
} }
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { export async function getAssignmentHandler(req: Request, res: Response): Promise<void> {
const id = Number(req.params.id); const id = Number(req.params.id);
const classid = req.params.classid; const classid = req.params.classid;
requireFields({ id, classid });
if (isNaN(id)) { if (isNaN(id)) {
res.status(400).json({ error: 'Assignment id must be a number' }); throw new BadRequestException('Assignment id should be a number');
return;
} }
const assignment = await getAssignment(classid, id); const assignment = await getAssignment(classid, id);
if (!assignment) { res.json({ assignment });
res.status(404).json({ error: 'Assignment not found' });
return;
}
res.json(assignment);
} }
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { export async function putAssignmentHandler(req: Request, res: Response): Promise<void> {
const id = Number(req.params.id);
const classid = req.params.classid;
requireFields({ id, classid });
if (isNaN(id)) {
throw new BadRequestException('Assignment id should be a number');
}
const assignmentData = req.body as Partial<EntityDTO<Assignment>>;
const assignment = await putAssignment(classid, id, assignmentData);
res.json({ assignment });
}
export async function deleteAssignmentHandler(req: Request, _res: Response): Promise<void> {
const id = Number(req.params.id);
const classid = req.params.classid;
requireFields({ id, classid });
if (isNaN(id)) {
throw new BadRequestException('Assignment id should be a number');
}
await deleteAssignment(classid, id);
}
export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid; const classid = req.params.classid;
const assignmentNumber = Number(req.params.id); const assignmentNumber = Number(req.params.id);
const full = req.query.full === 'true'; const full = req.query.full === 'true';
requireFields({ assignmentNumber, classid });
if (isNaN(assignmentNumber)) { if (isNaN(assignmentNumber)) {
res.status(400).json({ error: 'Assignment id must be a number' }); throw new BadRequestException('Assignment id should be a number');
return;
} }
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full); const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full);
res.json({ res.json({ submissions });
submissions: submissions,
});
} }

View file

@ -1,66 +1,132 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/classes.js'; import {
addClassStudent,
addClassTeacher,
createClass,
deleteClass,
deleteClassStudent,
deleteClassTeacher,
getAllClasses,
getClass,
getClassStudents,
getClassTeacherInvitations,
getClassTeachers,
putClass,
} from '../services/classes.js';
import { ClassDTO } from '@dwengo-1/common/interfaces/class'; import { ClassDTO } from '@dwengo-1/common/interfaces/class';
import { requireFields } from './error-helper.js';
import { EntityDTO } from '@mikro-orm/core';
import { Class } from '../entities/classes/class.entity.js';
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> { export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const classes = await getAllClasses(full); const classes = await getAllClasses(full);
res.json({ res.json({ classes });
classes: classes,
});
} }
export async function createClassHandler(req: Request, res: Response): Promise<void> { export async function createClassHandler(req: Request, res: Response): Promise<void> {
const displayName = req.body.displayName;
requireFields({ displayName });
const classData = req.body as ClassDTO; 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); const cls = await createClass(classData);
if (!cls) { res.json({ class: cls });
res.status(500).json({ error: 'Something went wrong while creating class' });
return;
}
res.status(201).json(cls);
} }
export async function getClassHandler(req: Request, res: Response): Promise<void> { export async function getClassHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id; const classId = req.params.id;
requireFields({ classId });
const cls = await getClass(classId); const cls = await getClass(classId);
if (!cls) { res.json({ class: cls });
res.status(404).json({ error: 'Class not found' }); }
return;
}
res.json(cls); export async function putClassHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
requireFields({ classId });
const newData = req.body as Partial<EntityDTO<Class>>;
const cls = await putClass(classId, newData);
res.json({ class: cls });
}
export async function deleteClassHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const cls = await deleteClass(classId);
res.json({ class: cls });
} }
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> { export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id; const classId = req.params.id;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
requireFields({ classId });
const students = full ? await getClassStudents(classId) : await getClassStudentsIds(classId); const students = await getClassStudents(classId, full);
res.json({ res.json({ students });
students: students, }
});
export async function getClassTeachersHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const full = req.query.full === 'true';
requireFields({ classId });
const teachers = await getClassTeachers(classId, full);
res.json({ teachers });
} }
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> { export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id; const classId = req.params.id;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
requireFields({ classId });
const invitations = await getClassTeacherInvitations(classId, full); const invitations = await getClassTeacherInvitations(classId, full);
res.json({ res.json({ invitations });
invitations: invitations, }
});
export async function deleteClassStudentHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const username = req.params.username;
requireFields({ classId, username });
const cls = await deleteClassStudent(classId, username);
res.json({ class: cls });
}
export async function deleteClassTeacherHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const username = req.params.username;
requireFields({ classId, username });
const cls = await deleteClassTeacher(classId, username);
res.json({ class: cls });
}
export async function addClassStudentHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const username = req.body.username;
requireFields({ classId, username });
const cls = await addClassStudent(classId, username);
res.json({ class: cls });
}
export async function addClassTeacherHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const username = req.body.username;
requireFields({ classId, username });
const cls = await addClassTeacher(classId, username);
res.json({ class: cls });
} }

View file

@ -1,100 +1,104 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js'; import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupSubmissions, putGroup } from '../services/groups.js';
import { GroupDTO } from '@dwengo-1/common/interfaces/group'; import { GroupDTO } from '@dwengo-1/common/interfaces/group';
import { requireFields } from './error-helper.js';
import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { EntityDTO } from '@mikro-orm/core';
import { Group } from '../entities/assignments/group.entity.js';
// Typescript is annoywith with parameter forwarding from class.ts function checkGroupFields(classId: string, assignmentId: number, groupId: number): void {
interface GroupParams { requireFields({ classId, assignmentId, groupId });
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 = Number(req.params.assignmentid);
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' }); throw new BadRequestException('Assignment id must be a number');
return;
} }
const groupId = Number(req.params.groupid!); // Can't be undefined
if (isNaN(groupId)) { if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' }); throw new BadRequestException('Group id must be a number');
return;
} }
}
const group = await getGroup(classId, assignmentId, groupId, full); export async function getGroupHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid;
const assignmentId = parseInt(req.params.assignmentid);
const groupId = parseInt(req.params.groupid);
checkGroupFields(classId, assignmentId, groupId);
if (!group) { const group = await getGroup(classId, assignmentId, groupId);
res.status(404).json({ error: 'Group not found' });
return;
}
res.json(group); res.json({ group });
}
export async function putGroupHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid;
const assignmentId = parseInt(req.params.assignmentid);
const groupId = parseInt(req.params.groupid);
checkGroupFields(classId, assignmentId, groupId);
const group = await putGroup(classId, assignmentId, groupId, req.body as Partial<EntityDTO<Group>>);
res.json({ group });
}
export async function deleteGroupHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid;
const assignmentId = parseInt(req.params.assignmentid);
const groupId = parseInt(req.params.groupid);
checkGroupFields(classId, assignmentId, groupId);
const group = await deleteGroup(classId, assignmentId, groupId);
res.json({ group });
} }
export async function getAllGroupsHandler(req: Request, res: Response): Promise<void> { export async function getAllGroupsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid; const classId = req.params.classid;
const full = req.query.full === 'true';
const assignmentId = Number(req.params.assignmentid); const assignmentId = Number(req.params.assignmentid);
const full = req.query.full === 'true';
requireFields({ classId, assignmentId });
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' }); throw new BadRequestException('Assignment id must be a number');
return;
} }
const groups = await getAllGroups(classId, assignmentId, full); const groups = await getAllGroups(classId, assignmentId, full);
res.json({ res.json({ groups });
groups: groups,
});
} }
export async function createGroupHandler(req: Request, res: Response): Promise<void> { export async function createGroupHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid; const classid = req.params.classid;
const assignmentId = Number(req.params.assignmentid); const assignmentId = Number(req.params.assignmentid);
requireFields({ classid, assignmentId });
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' }); throw new BadRequestException('Assignment id must be a number');
return;
} }
const groupData = req.body as GroupDTO; const groupData = req.body as GroupDTO;
const group = await createGroup(groupData, classid, assignmentId); const group = await createGroup(groupData, classid, assignmentId);
if (!group) { res.status(201).json({ group });
res.status(500).json({ error: 'Something went wrong while creating group' });
return;
}
res.status(201).json(group);
} }
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> { export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid; const classId = req.params.classid;
const assignmentId = Number(req.params.assignmentid);
const groupId = Number(req.params.groupid);
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const assignmentId = Number(req.params.assignmentid); requireFields({ classId, assignmentId, groupId });
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' }); throw new BadRequestException('Assignment id must be a number');
return;
} }
const groupId = Number(req.params.groupid); // Can't be undefined
if (isNaN(groupId)) { if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' }); throw new BadRequestException('Group id must be a number');
return;
} }
const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full); const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full);
res.json({ res.json({ submissions });
submissions: submissions,
});
} }

View file

@ -6,9 +6,9 @@ import attachmentService from '../services/learning-objects/attachment-service.j
import { BadRequestException } from '../exceptions/bad-request-exception.js'; import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { NotFoundException } from '../exceptions/not-found-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js';
import { envVars, getEnvVar } from '../util/envVars.js'; import { envVars, getEnvVar } from '../util/envVars.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO {
if (!req.params.hruid) { if (!req.params.hruid) {
throw new BadRequestException('HRUID is required.'); throw new BadRequestException('HRUID is required.');
} }

View file

@ -1,34 +1,27 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; import {
import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; createQuestion,
deleteQuestion,
getAllQuestions,
getQuestion,
getQuestionsAboutLearningObjectInAssignment,
updateQuestion,
} from '../services/questions.js';
import { FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM } from '../config.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { requireFields } from './error-helper.js';
function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier {
const { hruid, version } = req.params;
const lang = req.query.lang;
if (!hruid || !version) {
res.status(400).json({ error: 'Missing required parameters.' });
return null;
}
return { return {
hruid, hruid,
language: (lang as Language) || FALLBACK_LANG, language: (lang || FALLBACK_LANG) as Language,
version: Number(version), version: Number(version) || FALLBACK_VERSION_NUM,
}; };
} }
function getQuestionId(req: Request, res: Response): QuestionId | null { export function getQuestionId(learningObjectIdentifier: LearningObjectIdentifier, seq: string): QuestionId {
const seq = req.params.seq;
const learningObjectIdentifier = getObjectId(req, res);
if (!learningObjectIdentifier) {
return null;
}
return { return {
learningObjectIdentifier, learningObjectIdentifier,
sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM,
@ -36,84 +29,96 @@ function getQuestionId(req: Request, res: Response): QuestionId | null {
} }
export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> { export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> {
const objectId = getObjectId(req, res); const hruid = req.params.hruid;
const version = req.params.version;
const language = req.query.lang as string;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
requireFields({ hruid });
if (!objectId) { const learningObjectId = getLearningObjectId(hruid, version, language);
return;
}
const questions = await getAllQuestions(objectId, full); let questions: QuestionDTO[] | QuestionId[];
if (req.query.classId && req.query.assignmentId) {
if (!questions) { questions = await getQuestionsAboutLearningObjectInAssignment(
res.status(404).json({ error: `Questions not found.` }); learningObjectId,
req.query.classId as string,
parseInt(req.query.assignmentId as string),
full ?? false,
req.query.forStudent as string | undefined
);
} else { } else {
res.json({ questions: questions }); questions = await getAllQuestions(learningObjectId, full ?? false);
} }
res.json({ questions });
} }
export async function getQuestionHandler(req: Request, res: Response): Promise<void> { export async function getQuestionHandler(req: Request, res: Response): Promise<void> {
const questionId = getQuestionId(req, res); const hruid = req.params.hruid;
const version = req.params.version;
const language = req.query.lang as string;
const seq = req.params.seq;
requireFields({ hruid });
if (!questionId) { const learningObjectId = getLearningObjectId(hruid, version, language);
return; const questionId = getQuestionId(learningObjectId, seq);
}
const question = await getQuestion(questionId); const question = await getQuestion(questionId);
if (!question) { res.json({ 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 = await getAnswersByQuestion(questionId, full);
if (!answers) {
res.status(404).json({ error: `Questions not found` });
} else {
res.json({ answers: answers });
}
} }
export async function createQuestionHandler(req: Request, res: Response): Promise<void> { export async function createQuestionHandler(req: Request, res: Response): Promise<void> {
const questionDTO = req.body as QuestionDTO; const hruid = req.params.hruid;
const version = req.params.version;
const language = req.query.lang as string;
requireFields({ hruid });
if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) { const loId = getLearningObjectId(hruid, version, language);
res.status(400).json({ error: 'Missing required fields: identifier and content' });
return;
}
const question = await createQuestion(questionDTO); const author = req.body.author as string;
const content = req.body.content as string;
const inGroup = req.body.inGroup;
requireFields({ author, content, inGroup });
if (!question) { const questionData = req.body as QuestionData;
res.status(400).json({ error: 'Could not create question' });
} else { const question = await createQuestion(loId, questionData);
res.json(question);
} res.json({ question });
} }
export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> { export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> {
const questionId = getQuestionId(req, res); const hruid = req.params.hruid;
const version = req.params.version;
const language = req.query.lang as string;
const seq = req.params.seq;
requireFields({ hruid });
if (!questionId) { const learningObjectId = getLearningObjectId(hruid, version, language);
return; const questionId = getQuestionId(learningObjectId, seq);
}
const question = await deleteQuestion(questionId); const question = await deleteQuestion(questionId);
if (!question) { res.json({ question });
res.status(400).json({ error: 'Could not find nor delete question' }); }
} else {
res.json(question); export async function updateQuestionHandler(req: Request, res: Response): Promise<void> {
} const hruid = req.params.hruid;
const version = req.params.version;
const language = req.query.lang as string;
const seq = req.params.seq;
requireFields({ hruid });
const learningObjectId = getLearningObjectId(hruid, version, language);
const questionId = getQuestionId(learningObjectId, seq);
const content = req.body.content as string;
requireFields({ content });
const questionData = req.body as QuestionData;
const question = await updateQuestion(questionId, questionData);
res.json({ question });
} }

View file

@ -1,61 +1,83 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js'; import {
createSubmission,
deleteSubmission,
getAllSubmissions,
getSubmission,
getSubmissionsForLearningObjectAndAssignment,
} from '../services/submissions.js';
import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission';
import { Language, languageMap } from '@dwengo-1/common/util/language'; import { Language, languageMap } from '@dwengo-1/common/util/language';
import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { requireFields } from './error-helper.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
interface SubmissionParams { export async function getSubmissionsHandler(req: Request, res: Response): Promise<void> {
hruid: string; const loHruid = req.params.hruid;
id: number; const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = parseInt(req.query.version as string) ?? 1;
const submissions = await getSubmissionsForLearningObjectAndAssignment(
loHruid,
lang,
version,
req.query.classId as string,
parseInt(req.query.assignmentId as string)
);
res.json(submissions);
} }
export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> { export async function getSubmissionHandler(req: Request, res: Response): Promise<void> {
const lohruid = req.params.hruid; const lohruid = req.params.hruid;
const submissionNumber = Number(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 lang = languageMap[req.query.language as string] || Language.Dutch;
const version = (req.query.version || 1) as number; const version = (req.query.version || 1) as number;
const submissionNumber = Number(req.params.id);
requireFields({ lohruid, submissionNumber });
const submission = await getSubmission(lohruid, lang, version, submissionNumber); if (isNaN(submissionNumber)) {
throw new BadRequestException('Submission number must be a number');
if (!submission) {
res.status(404).json({ error: 'Submission not found' });
return;
} }
res.json(submission); const loId = new LearningObjectIdentifier(lohruid, lang, version);
const submission = await getSubmission(loId, submissionNumber);
res.json({ submission });
} }
export async function getAllSubmissionsHandler(req: Request, res: Response): Promise<void> {
const lohruid = req.params.hruid;
const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = (req.query.version || 1) as number;
requireFields({ lohruid });
const loId = new LearningObjectIdentifier(lohruid, lang, version);
const submissions = await getAllSubmissions(loId);
res.json({ submissions });
}
// TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> { export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
const submissionDTO = req.body as SubmissionDTO; const submissionDTO = req.body as SubmissionDTO;
const submission = await createSubmission(submissionDTO); const submission = await createSubmission(submissionDTO);
if (!submission) { res.json({ submission });
res.status(400).json({ error: 'Failed to create submission' });
return;
}
res.json(submission);
} }
export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> { export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid; const hruid = req.params.hruid;
const submissionNumber = Number(req.params.id);
const lang = languageMap[req.query.language as string] || Language.Dutch; const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = (req.query.version || 1) as number; const version = (req.query.version || 1) as number;
const submissionNumber = Number(req.params.id);
requireFields({ hruid, submissionNumber });
const submission = await deleteSubmission(hruid, lang, version, submissionNumber); if (isNaN(submissionNumber)) {
throw new BadRequestException('Submission number must be a number');
if (!submission) {
res.status(404).json({ error: 'Submission not found' });
return;
} }
res.json(submission); const loId = new LearningObjectIdentifier(hruid, lang, version);
const submission = await deleteSubmission(loId, submissionNumber);
res.json({ submission });
} }

View file

@ -81,16 +81,15 @@ export async function getTeacherQuestionHandler(req: Request, res: Response): Pr
} }
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> { export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
const username = req.query.username as string;
const classId = req.params.classId; const classId = req.params.classId;
requireFields({ username, classId }); requireFields({ classId });
const joinRequests = await getJoinRequestsByClass(classId); const joinRequests = await getJoinRequestsByClass(classId);
res.json({ joinRequests }); res.json({ joinRequests });
} }
export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise<void> { export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
const studentUsername = req.query.studentUsername as string; const studentUsername = req.params.studentUsername;
const classId = req.params.classId; const classId = req.params.classId;
const accepted = req.body.accepted !== 'false'; // Default = true const accepted = req.body.accepted !== 'false'; // Default = true
requireFields({ studentUsername, classId }); requireFields({ studentUsername, classId });

View file

@ -6,6 +6,22 @@ export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> { public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
return this.findOne({ within: within, id: id }); return this.findOne({ within: within, id: id });
} }
public async findByClassIdAndAssignmentId(withinClass: string, id: number): Promise<Assignment | null> {
return this.findOne({ within: { classId: withinClass }, id: id });
}
public async findAllByResponsibleTeacher(teacherUsername: string): Promise<Assignment[]> {
return this.findAll({
where: {
within: {
teachers: {
$some: {
username: teacherUsername,
},
},
},
},
});
}
public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
return this.findAll({ where: { within: within } }); return this.findAll({ where: { within: within } });
} }

View file

@ -3,6 +3,7 @@ import { Group } from '../../entities/assignments/group.entity.js';
import { Submission } from '../../entities/assignments/submission.entity.js'; import { Submission } from '../../entities/assignments/submission.entity.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
import { Assignment } from '../../entities/assignments/assignment.entity';
export class SubmissionRepository extends DwengoEntityRepository<Submission> { export class SubmissionRepository extends DwengoEntityRepository<Submission> {
public async findSubmissionByLearningObjectAndSubmissionNumber( public async findSubmissionByLearningObjectAndSubmissionNumber(
@ -17,6 +18,14 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
}); });
} }
public async findByLearningObject(loId: LearningObjectIdentifier): Promise<Submission[]> {
return this.find({
learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language,
learningObjectVersion: loId.version,
});
}
public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> { public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
return this.findOne( return this.findOne(
{ {
@ -42,11 +51,58 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
} }
public async findAllSubmissionsForGroup(group: Group): Promise<Submission[]> { public async findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
return this.find({ onBehalfOf: group }); return this.find(
{ onBehalfOf: group },
{
populate: ['onBehalfOf.members'],
}
);
}
/**
* Looks up all submissions for the given learning object which were submitted as part of the given assignment.
* When forStudentUsername is set, only the submissions of the given user's group are shown.
*/
public async findAllSubmissionsForLearningObjectAndAssignment(
loId: LearningObjectIdentifier,
assignment: Assignment,
forStudentUsername?: string
): Promise<Submission[]> {
const onBehalfOf = forStudentUsername
? {
assignment,
members: {
$some: {
username: forStudentUsername,
},
},
}
: {
assignment,
};
return this.findAll({
where: {
learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language,
learningObjectVersion: loId.version,
onBehalfOf,
},
});
} }
public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> { public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
return this.find({ submitter: student }); const result = await this.find(
{ submitter: student },
{
populate: ['onBehalfOf.members'],
}
);
// Workaround: For some reason, without this MikroORM generates an UPDATE query with a syntax error in some tests
this.em.clear();
return result;
} }
public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> { public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {

View file

@ -2,6 +2,7 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Answer } from '../../entities/questions/answer.entity.js'; import { Answer } from '../../entities/questions/answer.entity.js';
import { Question } from '../../entities/questions/question.entity.js'; import { Question } from '../../entities/questions/question.entity.js';
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
import { Loaded } from '@mikro-orm/core';
export class AnswerRepository extends DwengoEntityRepository<Answer> { export class AnswerRepository extends DwengoEntityRepository<Answer> {
public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> { public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
@ -19,10 +20,21 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> {
orderBy: { sequenceNumber: 'ASC' }, orderBy: { sequenceNumber: 'ASC' },
}); });
} }
public async findAnswer(question: Question, sequenceNumber: number): Promise<Loaded<Answer> | null> {
return this.findOne({
toQuestion: question,
sequenceNumber,
});
}
public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
toQuestion: question, toQuestion: question,
sequenceNumber: sequenceNumber, sequenceNumber: sequenceNumber,
}); });
} }
public async updateContent(answer: Answer, newContent: string): Promise<Answer> {
answer.content = newContent;
await this.save(answer);
return answer;
}
} }

View file

@ -3,14 +3,18 @@ import { Question } from '../../entities/questions/question.entity.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js';
import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Loaded } from '@mikro-orm/core';
import { Group } from '../../entities/assignments/group.entity';
export class QuestionRepository extends DwengoEntityRepository<Question> { export class QuestionRepository extends DwengoEntityRepository<Question> {
public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> { public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> {
const questionEntity = this.create({ const questionEntity = this.create({
learningObjectHruid: question.loId.hruid, learningObjectHruid: question.loId.hruid,
learningObjectLanguage: question.loId.language, learningObjectLanguage: question.loId.language,
learningObjectVersion: question.loId.version, learningObjectVersion: question.loId.version,
author: question.author, author: question.author,
inGroup: question.inGroup,
content: question.content, content: question.content,
timestamp: new Date(), timestamp: new Date(),
}); });
@ -18,6 +22,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
questionEntity.learningObjectLanguage = question.loId.language; questionEntity.learningObjectLanguage = question.loId.language;
questionEntity.learningObjectVersion = question.loId.version; questionEntity.learningObjectVersion = question.loId.version;
questionEntity.author = question.author; questionEntity.author = question.author;
questionEntity.inGroup = question.inGroup;
questionEntity.content = question.content; questionEntity.content = question.content;
return this.insert(questionEntity); return this.insert(questionEntity);
} }
@ -55,10 +60,67 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
}); });
} }
public async findAllByAssignment(assignment: Assignment): Promise<Question[]> {
return this.find({
inGroup: {
$contained: assignment.groups,
},
learningObjectHruid: assignment.learningPathHruid,
learningObjectLanguage: assignment.learningPathLanguage,
});
}
public async findAllByAuthor(author: Student): Promise<Question[]> { public async findAllByAuthor(author: Student): Promise<Question[]> {
return this.findAll({ return this.findAll({
where: { author }, where: { author },
orderBy: { timestamp: 'DESC' }, // New to old orderBy: { timestamp: 'DESC' }, // New to old
}); });
} }
/**
* Looks up all questions for the given learning object which were asked as part of the given assignment.
* When forStudentUsername is set, only the questions within the given user's group are shown.
*/
public async findAllQuestionsAboutLearningObjectInAssignment(
loId: LearningObjectIdentifier,
assignment: Assignment,
forStudentUsername?: string
): Promise<Question[]> {
const inGroup = forStudentUsername
? {
assignment,
members: {
$some: {
username: forStudentUsername,
},
},
}
: {
assignment,
};
return this.findAll({
where: {
learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language,
learningObjectVersion: loId.version,
inGroup,
},
});
}
public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<Loaded<Question> | null> {
return this.findOne({
learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language,
learningObjectVersion: loId.version,
sequenceNumber,
});
}
public async updateContent(question: Question, newContent: string): Promise<Question> {
question.content = newContent;
await this.save(question);
return question;
}
} }

View file

@ -1,4 +1,4 @@
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 { Class } from '../classes/class.entity.js';
import { Group } from './group.entity.js'; import { Group } from './group.entity.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
@ -35,5 +35,5 @@ export class Assignment {
entity: () => Group, entity: () => Group,
mappedBy: 'assignment', mappedBy: 'assignment',
}) })
groups!: Group[]; groups!: Collection<Group>;
} }

View file

@ -1,4 +1,4 @@
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 { Assignment } from './assignment.entity.js';
import { Student } from '../users/student.entity.js'; import { Student } from '../users/student.entity.js';
import { GroupRepository } from '../../data/assignments/group-repository.js'; import { GroupRepository } from '../../data/assignments/group-repository.js';
@ -19,5 +19,5 @@ export class Group {
@ManyToMany({ @ManyToMany({
entity: () => Student, entity: () => Student,
}) })
members!: Student[]; members!: Collection<Student>;
} }

View file

@ -21,6 +21,11 @@ export class Submission {
@PrimaryKey({ type: 'integer', autoincrement: true }) @PrimaryKey({ type: 'integer', autoincrement: true })
submissionNumber?: number; submissionNumber?: number;
@ManyToOne({
entity: () => Group,
})
onBehalfOf!: Group;
@ManyToOne({ @ManyToOne({
entity: () => Student, entity: () => Student,
}) })
@ -29,12 +34,6 @@ export class Submission {
@Property({ type: 'datetime' }) @Property({ type: 'datetime' })
submissionTime!: Date; submissionTime!: Date;
@ManyToOne({
entity: () => Group,
nullable: true,
})
onBehalfOf?: Group;
@Property({ type: 'json' }) @Property({ type: 'json' })
content!: string; content!: string;
} }

View file

@ -2,6 +2,7 @@ import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
import { Student } from '../users/student.entity.js'; import { Student } from '../users/student.entity.js';
import { QuestionRepository } from '../../data/questions/question-repository.js'; import { QuestionRepository } from '../../data/questions/question-repository.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { Group } from '../assignments/group.entity.js';
@Entity({ repository: () => QuestionRepository }) @Entity({ repository: () => QuestionRepository })
export class Question { export class Question {
@ -20,6 +21,9 @@ export class Question {
@PrimaryKey({ type: 'integer', autoincrement: true }) @PrimaryKey({ type: 'integer', autoincrement: true })
sequenceNumber?: number; sequenceNumber?: number;
@ManyToOne({ entity: () => Group })
inGroup!: Group;
@ManyToOne({ @ManyToOne({
entity: () => Student, entity: () => Student,
}) })

View file

@ -1,14 +1,14 @@
import { mapToUserDTO } from './user.js';
import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js';
import { Answer } from '../entities/questions/answer.entity.js'; import { Answer } from '../entities/questions/answer.entity.js';
import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
import { mapToTeacherDTO } from './teacher.js';
/** /**
* Convert a Question entity to a DTO format. * Convert a Question entity to a DTO format.
*/ */
export function mapToAnswerDTO(answer: Answer): AnswerDTO { export function mapToAnswerDTO(answer: Answer): AnswerDTO {
return { return {
author: mapToUserDTO(answer.author), author: mapToTeacherDTO(answer.author),
toQuestion: mapToQuestionDTO(answer.toQuestion), toQuestion: mapToQuestionDTO(answer.toQuestion),
sequenceNumber: answer.sequenceNumber!, sequenceNumber: answer.sequenceNumber!,
timestamp: answer.timestamp.toISOString(), timestamp: answer.timestamp.toISOString(),

View file

@ -8,19 +8,18 @@ import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO { export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO {
return { return {
id: assignment.id!, id: assignment.id!,
class: assignment.within.classId!, within: assignment.within.classId!,
title: assignment.title, title: assignment.title,
description: assignment.description, description: assignment.description,
learningPath: assignment.learningPathHruid, learningPath: assignment.learningPathHruid,
language: assignment.learningPathLanguage, language: assignment.learningPathLanguage,
// Groups: assignment.groups.map(group => group.groupNumber),
}; };
} }
export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO { export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
return { return {
id: assignment.id!, id: assignment.id!,
class: assignment.within.classId!, within: assignment.within.classId!,
title: assignment.title, title: assignment.title,
description: assignment.description, description: assignment.description,
learningPath: assignment.learningPathHruid, learningPath: assignment.learningPathHruid,

View file

@ -10,7 +10,6 @@ export function mapToClassDTO(cls: Class): ClassDTO {
displayName: cls.displayName, displayName: cls.displayName,
teachers: cls.teachers.map((teacher) => teacher.username), teachers: cls.teachers.map((teacher) => teacher.username),
students: cls.students.map((student) => student.username), students: cls.students.map((student) => student.username),
joinRequests: [], // TODO
}; };
} }

View file

@ -1,11 +1,29 @@
import { Group } from '../entities/assignments/group.entity.js'; import { Group } from '../entities/assignments/group.entity.js';
import { mapToAssignment } from './assignment.js';
import { mapToStudent } from './student.js';
import { mapToAssignmentDTO } from './assignment.js'; import { mapToAssignmentDTO } from './assignment.js';
import { mapToStudentDTO } from './student.js'; import { mapToStudentDTO } from './student.js';
import { GroupDTO } from '@dwengo-1/common/interfaces/group'; import { GroupDTO } from '@dwengo-1/common/interfaces/group';
import { getGroupRepository } from '../data/repositories.js';
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
import { Class } from '../entities/classes/class.entity.js';
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { mapToClassDTO } from './class';
export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group {
const assignmentDto = groupDto.assignment as AssignmentDTO;
return getGroupRepository().create({
groupNumber: groupDto.groupNumber,
assignment: mapToAssignment(assignmentDto, clazz),
members: groupDto.members!.map((studentDto) => mapToStudent(studentDto as StudentDTO)),
});
}
export function mapToGroupDTO(group: Group): GroupDTO { export function mapToGroupDTO(group: Group): GroupDTO {
return { return {
assignment: mapToAssignmentDTO(group.assignment), // ERROR: , group.assignment.within), class: mapToClassDTO(group.assignment.within),
assignment: mapToAssignmentDTO(group.assignment),
groupNumber: group.groupNumber!, groupNumber: group.groupNumber!,
members: group.members.map(mapToStudentDTO), members: group.members.map(mapToStudentDTO),
}; };
@ -13,6 +31,18 @@ export function mapToGroupDTO(group: Group): GroupDTO {
export function mapToGroupDTOId(group: Group): GroupDTO { export function mapToGroupDTOId(group: Group): GroupDTO {
return { return {
class: group.assignment.within.classId!,
assignment: group.assignment.id!,
groupNumber: group.groupNumber!,
};
}
/**
* Map to group DTO where other objects are only referenced by their id.
*/
export function mapToShallowGroupDTO(group: Group): GroupDTO {
return {
class: group.assignment.within.classId!,
assignment: group.assignment.id!, assignment: group.assignment.id!,
groupNumber: group.groupNumber!, groupNumber: group.groupNumber!,
members: group.members.map((member) => member.username), members: group.members.map((member) => member.username),

View file

@ -1,9 +1,11 @@
import { Question } from '../entities/questions/question.entity.js'; import { Question } from '../entities/questions/question.entity.js';
import { mapToStudentDTO } from './student.js'; import { mapToStudentDTO } from './student.js';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToGroupDTOId } from './group.js';
function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { function getLearningObjectIdentifier(question: Question): LearningObjectIdentifierDTO {
return { return {
hruid: question.learningObjectHruid, hruid: question.learningObjectHruid,
language: question.learningObjectLanguage, language: question.learningObjectLanguage,
@ -11,6 +13,14 @@ function getLearningObjectIdentifier(question: Question): LearningObjectIdentifi
}; };
} }
export function mapToLearningObjectID(loID: LearningObjectIdentifierDTO): LearningObjectIdentifier {
return {
hruid: loID.hruid,
language: loID.language,
version: loID.version ?? 1,
};
}
/** /**
* Convert a Question entity to a DTO format. * Convert a Question entity to a DTO format.
*/ */
@ -21,6 +31,7 @@ export function mapToQuestionDTO(question: Question): QuestionDTO {
learningObjectIdentifier, learningObjectIdentifier,
sequenceNumber: question.sequenceNumber!, sequenceNumber: question.sequenceNumber!,
author: mapToStudentDTO(question.author), author: mapToStudentDTO(question.author),
inGroup: mapToGroupDTOId(question.inGroup),
timestamp: question.timestamp.toISOString(), timestamp: question.timestamp.toISOString(),
content: question.content, content: question.content,
}; };

View file

@ -1,7 +1,10 @@
import { Submission } from '../entities/assignments/submission.entity.js'; import { Submission } from '../entities/assignments/submission.entity.js';
import { mapToGroupDTO } from './group.js'; import { mapToGroupDTO } from './group.js';
import { mapToStudent, mapToStudentDTO } from './student.js'; import { mapToStudentDTO } from './student.js';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { getSubmissionRepository } from '../data/repositories';
import { Student } from '../entities/users/student.entity';
import { Group } from '../entities/assignments/group.entity';
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
return { return {
@ -14,7 +17,7 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
submissionNumber: submission.submissionNumber, submissionNumber: submission.submissionNumber,
submitter: mapToStudentDTO(submission.submitter), submitter: mapToStudentDTO(submission.submitter),
time: submission.submissionTime, time: submission.submissionTime,
group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf) : undefined, group: mapToGroupDTO(submission.onBehalfOf),
content: submission.content, content: submission.content,
}; };
} }
@ -29,17 +32,14 @@ export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId {
}; };
} }
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission { export function mapToSubmission(submissionDTO: SubmissionDTO, submitter: Student, onBehalfOf: Group): Submission {
const submission = new Submission(); return getSubmissionRepository().create({
submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid; learningObjectHruid: submissionDTO.learningObjectIdentifier.hruid,
submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language; learningObjectLanguage: submissionDTO.learningObjectIdentifier.language,
submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!; learningObjectVersion: submissionDTO.learningObjectIdentifier.version || 1,
// Submission.submissionNumber = submissionDTO.submissionNumber; submitter: submitter,
submission.submitter = mapToStudent(submissionDTO.submitter); submissionTime: new Date(),
// Submission.submissionTime = submissionDTO.time; content: submissionDTO.content,
// Submission.onBehalfOf = submissionDTO.group!; onBehalfOf: onBehalfOf,
// TODO fix group });
submission.content = submissionDTO.content;
return submission;
} }

View file

@ -1,10 +1,10 @@
import { EntityManager, MikroORM } from '@mikro-orm/core'; import { EntityManager, IDatabaseDriver, MikroORM } from '@mikro-orm/core';
import config from './mikro-orm.config.js'; import config from './mikro-orm.config.js';
import { envVars, getEnvVar } from './util/envVars.js'; import { envVars, getEnvVar } from './util/envVars.js';
import { getLogger, Logger } from './logging/initalize.js'; import { getLogger, Logger } from './logging/initalize.js';
let orm: MikroORM | undefined; let orm: MikroORM | undefined;
export async function initORM(testingMode = false): Promise<void> { export async function initORM(testingMode = false): Promise<MikroORM<IDatabaseDriver, EntityManager>> {
const logger: Logger = getLogger(); const logger: Logger = getLogger();
logger.info('Initializing ORM'); logger.info('Initializing ORM');
@ -25,6 +25,8 @@ export async function initORM(testingMode = false): Promise<void> {
); );
} }
} }
return orm;
} }
export function forkEntityManager(): EntityManager { export function forkEntityManager(): EntityManager {
if (!orm) { if (!orm) {

View file

@ -0,0 +1,16 @@
import express from 'express';
import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js';
const router = express.Router({ mergeParams: true });
router.get('/', getAllAnswersHandler);
router.post('/', createAnswerHandler);
router.get('/:seqAnswer', getAnswerHandler);
router.delete('/:seqAnswer', deleteAnswerHandler);
router.put('/:seqAnswer', updateAnswerHandler);
export default router;

View file

@ -1,22 +1,26 @@
import express from 'express'; import express from 'express';
import { import {
createAssignmentHandler, createAssignmentHandler,
deleteAssignmentHandler,
getAllAssignmentsHandler, getAllAssignmentsHandler,
getAssignmentHandler, getAssignmentHandler,
getAssignmentsSubmissionsHandler, getAssignmentsSubmissionsHandler,
putAssignmentHandler,
} from '../controllers/assignments.js'; } from '../controllers/assignments.js';
import groupRouter from './groups.js'; import groupRouter from './groups.js';
const router = express.Router({ mergeParams: true }); const router = express.Router({ mergeParams: true });
// Root endpoint used to search objects
router.get('/', getAllAssignmentsHandler); router.get('/', getAllAssignmentsHandler);
router.post('/', createAssignmentHandler); router.post('/', createAssignmentHandler);
// Information about an assignment with id 'id'
router.get('/:id', getAssignmentHandler); router.get('/:id', getAssignmentHandler);
router.put('/:id', putAssignmentHandler);
router.delete('/:id', deleteAssignmentHandler);
router.get('/:id/submissions', getAssignmentsSubmissionsHandler); router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
router.get('/:id/questions', (_req, res) => { router.get('/:id/questions', (_req, res) => {

View file

@ -1,10 +1,17 @@
import express from 'express'; import express from 'express';
import { import {
addClassStudentHandler,
addClassTeacherHandler,
createClassHandler, createClassHandler,
deleteClassHandler,
deleteClassStudentHandler,
deleteClassTeacherHandler,
getAllClassesHandler, getAllClassesHandler,
getClassHandler, getClassHandler,
getClassStudentsHandler, getClassStudentsHandler,
getClassTeachersHandler,
getTeacherInvitationsHandler, getTeacherInvitationsHandler,
putClassHandler,
} from '../controllers/classes.js'; } from '../controllers/classes.js';
import assignmentRouter from './assignments.js'; import assignmentRouter from './assignments.js';
@ -15,13 +22,26 @@ router.get('/', getAllClassesHandler);
router.post('/', createClassHandler); router.post('/', createClassHandler);
// Information about an class with id 'id'
router.get('/:id', getClassHandler); router.get('/:id', getClassHandler);
router.put('/:id', putClassHandler);
router.delete('/:id', deleteClassHandler);
router.get('/:id/teacher-invitations', getTeacherInvitationsHandler); router.get('/:id/teacher-invitations', getTeacherInvitationsHandler);
router.get('/:id/students', getClassStudentsHandler); router.get('/:id/students', getClassStudentsHandler);
router.post('/:id/students', addClassStudentHandler);
router.delete('/:id/students/:username', deleteClassStudentHandler);
router.get('/:id/teachers', getClassTeachersHandler);
router.post('/:id/teachers', addClassTeacherHandler);
router.delete('/:id/teachers/:username', deleteClassTeacherHandler);
router.use('/:classid/assignments', assignmentRouter); router.use('/:classid/assignments', assignmentRouter);
export default router; export default router;

View file

@ -1,5 +1,12 @@
import express from 'express'; import express from 'express';
import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js'; import {
createGroupHandler,
deleteGroupHandler,
getAllGroupsHandler,
getGroupHandler,
getGroupSubmissionsHandler,
putGroupHandler,
} from '../controllers/groups.js';
const router = express.Router({ mergeParams: true }); const router = express.Router({ mergeParams: true });
@ -8,16 +15,12 @@ router.get('/', getAllGroupsHandler);
router.post('/', createGroupHandler); router.post('/', createGroupHandler);
// Information about a group (members, ... [TODO DOC])
router.get('/:groupid', getGroupHandler); router.get('/:groupid', getGroupHandler);
router.get('/:groupid', getGroupSubmissionsHandler); router.put('/:groupid', putGroupHandler);
// The list of questions a group has made router.delete('/:groupid', deleteGroupHandler);
router.get('/:id/questions', (_req, res) => {
res.json({ router.get('/:groupid/submissions', getGroupSubmissionsHandler);
questions: ['0'],
});
});
export default router; export default router;

View file

@ -1,11 +1,7 @@
import express from 'express'; import express from 'express';
import { import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js';
createQuestionHandler, import answerRoutes from './answers.js';
deleteQuestionHandler,
getAllQuestionsHandler,
getQuestionAnswersHandler,
getQuestionHandler,
} from '../controllers/questions.js';
const router = express.Router({ mergeParams: true }); const router = express.Router({ mergeParams: true });
// Query language // Query language
@ -20,6 +16,6 @@ router.delete('/:seq', deleteQuestionHandler);
// Information about a question with id // Information about a question with id
router.get('/:seq', getQuestionHandler); router.get('/:seq', getQuestionHandler);
router.get('/answers/:seq', getQuestionAnswersHandler); router.use('/:seq/answers', answerRoutes);
export default router; export default router;

View file

@ -1,13 +1,9 @@
import express from 'express'; import express from 'express';
import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler } from '../controllers/submissions.js'; import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.js';
const router = express.Router({ mergeParams: true }); const router = express.Router({ mergeParams: true });
// Root endpoint used to search objects // Root endpoint used to search objects
router.get('/', (_req, res) => { router.get('/', getSubmissionsHandler);
res.json({
submissions: ['0', '1'],
});
});
router.post('/:id', createSubmissionHandler); router.post('/:id', createSubmissionHandler);

View file

@ -0,0 +1,70 @@
import { getAnswerRepository } from '../data/repositories.js';
import { Answer } from '../entities/questions/answer.entity.js';
import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js';
import { fetchTeacher } from './teachers.js';
import { fetchQuestion } from './questions.js';
import { QuestionId } from '@dwengo-1/common/interfaces/question';
import { AnswerData, AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
import { NotFoundException } from '../exceptions/not-found-exception.js';
export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise<AnswerDTO[] | AnswerId[]> {
const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId);
const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question);
if (full) {
return answers.map(mapToAnswerDTO);
}
return answers.map(mapToAnswerDTOId);
}
export async function createAnswer(questionId: QuestionId, answerData: AnswerData): Promise<AnswerDTO> {
const answerRepository = getAnswerRepository();
const toQuestion = await fetchQuestion(questionId);
const author = await fetchTeacher(answerData.author);
const content = answerData.content;
const answer = await answerRepository.createAnswer({
toQuestion,
author,
content,
});
return mapToAnswerDTO(answer);
}
async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> {
const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId);
const answer = await answerRepository.findAnswer(question, sequenceNumber);
if (!answer) {
throw new NotFoundException('Answer with questionID and sequence number not found');
}
return answer;
}
export async function getAnswer(questionId: QuestionId, sequenceNumber: number): Promise<AnswerDTO> {
const answer = await fetchAnswer(questionId, sequenceNumber);
return mapToAnswerDTO(answer);
}
export async function deleteAnswer(questionId: QuestionId, sequenceNumber: number): Promise<AnswerDTO> {
const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId);
const answer = await fetchAnswer(questionId, sequenceNumber);
await answerRepository.removeAnswerByQuestionAndSequenceNumber(question, sequenceNumber);
return mapToAnswerDTO(answer);
}
export async function updateAnswer(questionId: QuestionId, sequenceNumber: number, answerData: AnswerData): Promise<AnswerDTO> {
const answerRepository = getAnswerRepository();
const answer = await fetchAnswer(questionId, sequenceNumber);
const newAnswer = await answerRepository.updateContent(answer, answerData.content);
return mapToAnswerDTO(newAnswer);
}

View file

@ -1,18 +1,43 @@
import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
import {
getAssignmentRepository,
getClassRepository,
getGroupRepository,
getQuestionRepository,
getSubmissionRepository,
} from '../data/repositories.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
import { mapToQuestionDTO } from '../interfaces/question.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
import { fetchClass } from './classes.js';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { getLogger } from '../logging/initalize.js'; import { EntityDTO } from '@mikro-orm/core';
import { putObject } from './service-helper.js';
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> { export async function fetchAssignment(classid: string, assignmentNumber: number): Promise<Assignment> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
const cls = await classRepository.findById(classid); const cls = await classRepository.findById(classid);
if (!cls) { if (!cls) {
return []; throw new NotFoundException("Could not find assignment's class");
} }
const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
if (!assignment) {
throw new NotFoundException('Could not find assignment');
}
return assignment;
}
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
const cls = await fetchClass(classid);
const assignmentRepository = getAssignmentRepository(); const assignmentRepository = getAssignmentRepository();
const assignments = await assignmentRepository.findAllAssignmentsInClass(cls); const assignments = await assignmentRepository.findAllAssignmentsInClass(cls);
@ -23,42 +48,37 @@ export async function getAllAssignments(classid: string, full: boolean): Promise
return assignments.map(mapToAssignmentDTOId); return assignments.map(mapToAssignmentDTOId);
} }
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO | null> { export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO> {
const classRepository = getClassRepository(); const cls = await fetchClass(classid);
const cls = await classRepository.findById(classid);
if (!cls) {
return null;
}
const assignment = mapToAssignment(assignmentData, cls); const assignment = mapToAssignment(assignmentData, cls);
const assignmentRepository = getAssignmentRepository(); const assignmentRepository = getAssignmentRepository();
const newAssignment = assignmentRepository.create(assignment);
await assignmentRepository.save(newAssignment, { preventOverwrite: true });
try { return mapToAssignmentDTO(newAssignment);
const newAssignment = assignmentRepository.create(assignment);
await assignmentRepository.save(newAssignment);
return mapToAssignmentDTO(newAssignment);
} catch (e) {
getLogger().error(e);
return null;
}
} }
export async function getAssignment(classid: string, id: number): Promise<AssignmentDTO | null> { export async function getAssignment(classid: string, id: number): Promise<AssignmentDTO> {
const classRepository = getClassRepository(); const assignment = await fetchAssignment(classid, id);
const cls = await classRepository.findById(classid); return mapToAssignmentDTO(assignment);
}
if (!cls) { export async function putAssignment(classid: string, id: number, assignmentData: Partial<EntityDTO<Assignment>>): Promise<AssignmentDTO> {
return null; const assignment = await fetchAssignment(classid, id);
}
await putObject<Assignment>(assignment, assignmentData, getAssignmentRepository());
return mapToAssignmentDTO(assignment);
}
export async function deleteAssignment(classid: string, id: number): Promise<AssignmentDTO> {
const assignment = await fetchAssignment(classid, id);
const cls = await fetchClass(classid);
const assignmentRepository = getAssignmentRepository(); const assignmentRepository = getAssignmentRepository();
const assignment = await assignmentRepository.findByClassAndId(cls, id); await assignmentRepository.deleteByClassAndId(cls, id);
if (!assignment) {
return null;
}
return mapToAssignmentDTO(assignment); return mapToAssignmentDTO(assignment);
} }
@ -68,19 +88,7 @@ export async function getAssignmentsSubmissions(
assignmentNumber: number, assignmentNumber: number,
full: boolean full: boolean
): Promise<SubmissionDTO[] | SubmissionDTOId[]> { ): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const classRepository = getClassRepository(); const assignment = await fetchAssignment(classid, assignmentNumber);
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 groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsForAssignment(assignment); const groups = await groupRepository.findAllGroupsForAssignment(assignment);
@ -94,3 +102,16 @@ export async function getAssignmentsSubmissions(
return submissions.map(mapToSubmissionDTOId); return submissions.map(mapToSubmissionDTOId);
} }
export async function getAssignmentsQuestions(classid: string, assignmentNumber: number, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const assignment = await fetchAssignment(classid, assignmentNumber);
const questionRepository = getQuestionRepository();
const questions = await questionRepository.findAllByAssignment(assignment);
if (full) {
return questions.map(mapToQuestionDTO);
}
return questions.map(mapToQuestionDTO);
}

View file

@ -1,22 +1,25 @@
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js'; import { getClassRepository, getTeacherInvitationRepository } from '../data/repositories.js';
import { mapToClassDTO } from '../interfaces/class.js'; import { mapToClassDTO } from '../interfaces/class.js';
import { mapToStudentDTO } from '../interfaces/student.js'; import { mapToStudentDTO } from '../interfaces/student.js';
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.js'; import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.js';
import { getLogger } from '../logging/initalize.js';
import { NotFoundException } from '../exceptions/not-found-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js';
import { Class } from '../entities/classes/class.entity.js'; import { Class } from '../entities/classes/class.entity.js';
import { ClassDTO } from '@dwengo-1/common/interfaces/class'; import { ClassDTO } from '@dwengo-1/common/interfaces/class';
import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation';
import { StudentDTO } from '@dwengo-1/common/interfaces/student'; import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { fetchTeacher } from './teachers.js';
import { fetchStudent } from './students.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import { mapToTeacherDTO } from '../interfaces/teacher.js';
import { EntityDTO } from '@mikro-orm/core';
import { putObject } from './service-helper.js';
const logger = getLogger(); export async function fetchClass(classid: string): Promise<Class> {
export async function fetchClass(classId: string): Promise<Class> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
const cls = await classRepository.findById(classId); const cls = await classRepository.findById(classid);
if (!cls) { if (!cls) {
throw new NotFoundException('Class with id not found'); throw new NotFoundException('Class not found');
} }
return cls; return cls;
@ -24,11 +27,7 @@ export async function fetchClass(classId: string): Promise<Class> {
export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[]> { export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[]> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); const classes = await classRepository.findAll({ populate: ['students', 'teachers'] });
if (!classes) {
return [];
}
if (full) { if (full) {
return classes.map(mapToClassDTO); return classes.map(mapToClassDTO);
@ -36,74 +35,71 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[
return classes.map((cls) => cls.classId!); return classes.map((cls) => cls.classId!);
} }
export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> { export async function getClass(classId: string): Promise<ClassDTO> {
const teacherRepository = getTeacherRepository(); const cls = await fetchClass(classId);
const teacherUsernames = classData.teachers || []; return mapToClassDTO(cls);
const teachers = (await Promise.all(teacherUsernames.map(async (id) => teacherRepository.findByUsername(id)))).filter(
(teacher) => teacher !== null
);
const studentRepository = getStudentRepository();
const studentUsernames = classData.students || [];
const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter(
(student) => student !== null
);
const classRepository = getClassRepository();
try {
const newClass = classRepository.create({
displayName: classData.displayName,
teachers: teachers,
students: students,
});
await classRepository.save(newClass);
return mapToClassDTO(newClass);
} catch (e) {
logger.error(e);
return null;
}
} }
export async function getClass(classId: string): Promise<ClassDTO | null> { export async function createClass(classData: ClassDTO): Promise<ClassDTO> {
const classRepository = getClassRepository(); const teacherUsernames = classData.teachers || [];
const cls = await classRepository.findById(classId); const teachers = await Promise.all(teacherUsernames.map(async (id) => fetchTeacher(id)));
if (!cls) { const studentUsernames = classData.students || [];
return null; const students = await Promise.all(studentUsernames.map(async (id) => fetchStudent(id)));
}
const classRepository = getClassRepository();
const newClass = classRepository.create({
displayName: classData.displayName,
teachers: teachers,
students: students,
});
await classRepository.save(newClass, { preventOverwrite: true });
return mapToClassDTO(newClass);
}
export async function putClass(classId: string, classData: Partial<EntityDTO<Class>>): Promise<ClassDTO> {
const cls = await fetchClass(classId);
await putObject<Class>(cls, classData, getClassRepository());
return mapToClassDTO(cls); return mapToClassDTO(cls);
} }
async function fetchClassStudents(classId: string): Promise<StudentDTO[]> { export async function deleteClass(classId: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
const classRepository = getClassRepository(); const classRepository = getClassRepository();
const cls = await classRepository.findById(classId); await classRepository.deleteById(classId);
if (!cls) { return mapToClassDTO(cls);
return []; }
export async function getClassStudents(classId: string, full: boolean): Promise<StudentDTO[] | string[]> {
const cls = await fetchClass(classId);
if (full) {
return cls.students.map(mapToStudentDTO);
} }
return cls.students.map((student) => student.username);
}
export async function getClassStudentsDTO(classId: string): Promise<StudentDTO[]> {
const cls = await fetchClass(classId);
return cls.students.map(mapToStudentDTO); return cls.students.map(mapToStudentDTO);
} }
export async function getClassStudents(classId: string): Promise<StudentDTO[]> { export async function getClassTeachers(classId: string, full: boolean): Promise<TeacherDTO[] | string[]> {
return await fetchClassStudents(classId); const cls = await fetchClass(classId);
}
export async function getClassStudentsIds(classId: string): Promise<string[]> { if (full) {
const students: StudentDTO[] = await fetchClassStudents(classId); return cls.teachers.map(mapToTeacherDTO);
return students.map((student) => student.username); }
return cls.teachers.map((student) => student.username);
} }
export async function getClassTeacherInvitations(classId: string, full: boolean): Promise<TeacherInvitationDTO[]> { export async function getClassTeacherInvitations(classId: string, full: boolean): Promise<TeacherInvitationDTO[]> {
const classRepository = getClassRepository(); const cls = await fetchClass(classId);
const cls = await classRepository.findById(classId);
if (!cls) {
return [];
}
const teacherInvitationRepository = getTeacherInvitationRepository(); const teacherInvitationRepository = getTeacherInvitationRepository();
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(cls); const invitations = await teacherInvitationRepository.findAllInvitationsForClass(cls);
@ -114,3 +110,41 @@ export async function getClassTeacherInvitations(classId: string, full: boolean)
return invitations.map(mapToTeacherInvitationDTOIds); return invitations.map(mapToTeacherInvitationDTOIds);
} }
export async function deleteClassStudent(classId: string, username: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
const newStudents = { students: cls.students.filter((student) => student.username !== username) };
await putObject<Class>(cls, newStudents, getClassRepository());
return mapToClassDTO(cls);
}
export async function deleteClassTeacher(classId: string, username: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
const newTeachers = { teachers: cls.teachers.filter((teacher) => teacher.username !== username) };
await putObject<Class>(cls, newTeachers, getClassRepository());
return mapToClassDTO(cls);
}
export async function addClassStudent(classId: string, username: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
const newStudent = await fetchStudent(username);
const newStudents = { students: [...cls.students, newStudent] };
await putObject<Class>(cls, newStudents, getClassRepository());
return mapToClassDTO(cls);
}
export async function addClassTeacher(classId: string, username: string): Promise<ClassDTO> {
const cls = await fetchClass(classId);
const newTeacher = await fetchTeacher(username);
const newTeachers = { teachers: [...cls.teachers, newTeacher] };
await putObject<Class>(cls, newTeachers, getClassRepository());
return mapToClassDTO(cls);
}

View file

@ -1,109 +1,94 @@
import { import { EntityDTO } from '@mikro-orm/core';
getAssignmentRepository, import { getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js';
getClassRepository,
getGroupRepository,
getStudentRepository,
getSubmissionRepository,
} from '../data/repositories.js';
import { Group } from '../entities/assignments/group.entity.js'; import { Group } from '../entities/assignments/group.entity.js';
import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; import { mapToGroupDTO, mapToShallowGroupDTO } from '../interfaces/group.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
import { GroupDTO } from '@dwengo-1/common/interfaces/group'; import { GroupDTO } from '@dwengo-1/common/interfaces/group';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { getLogger } from '../logging/initalize.js'; import { fetchAssignment } from './assignments.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
import { putObject } from './service-helper.js';
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> { export async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<Group> {
const classRepository = getClassRepository(); const assignment = await fetchAssignment(classId, assignmentNumber);
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 groupRepository = getGroupRepository();
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber);
if (!group) { if (!group) {
return null; throw new NotFoundException('Could not find group');
} }
if (full) { return group;
return mapToGroupDTO(group);
}
return mapToGroupDTOId(group);
} }
export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise<Group | null> { export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> {
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
return mapToGroupDTO(group);
}
export async function putGroup(
classId: string,
assignmentNumber: number,
groupNumber: number,
groupData: Partial<EntityDTO<Group>>
): Promise<GroupDTO> {
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
await putObject<Group>(group, groupData, getGroupRepository());
return mapToGroupDTO(group);
}
export async function deleteGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> {
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
const assignment = await fetchAssignment(classId, assignmentNumber);
const groupRepository = getGroupRepository();
await groupRepository.deleteByAssignmentAndGroupNumber(assignment, groupNumber);
return mapToGroupDTO(group);
}
export async function getExistingGroupFromGroupDTO(groupData: GroupDTO): Promise<Group> {
const classId = typeof groupData.class === 'string' ? groupData.class : groupData.class.id;
const assignmentNumber = typeof groupData.assignment === 'number' ? groupData.assignment : groupData.assignment.id;
const groupNumber = groupData.groupNumber;
return await fetchGroup(classId, assignmentNumber, groupNumber);
}
export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise<GroupDTO> {
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list const memberUsernames = (groupData.members as string[]) || [];
const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter( const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter(
(student) => student !== null (student) => student !== null
); );
getLogger().debug(members); const assignment = await fetchAssignment(classid, assignmentNumber);
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 groupRepository = getGroupRepository();
try { const newGroup = groupRepository.create({
const newGroup = groupRepository.create({ assignment: assignment,
assignment: assignment, members: members,
members: members, });
}); await groupRepository.save(newGroup);
await groupRepository.save(newGroup);
return newGroup; return mapToGroupDTO(newGroup);
} catch (e) {
getLogger().error(e);
return null;
}
} }
export async function getAllGroups(classId: string, assignmentNumber: number, full: boolean): Promise<GroupDTO[]> { export async function getAllGroups(classId: string, assignmentNumber: number, full: boolean): Promise<GroupDTO[]> {
const classRepository = getClassRepository(); const assignment = await fetchAssignment(classId, assignmentNumber);
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 groupRepository = getGroupRepository();
const groups = await groupRepository.findAllGroupsForAssignment(assignment); const groups = await groupRepository.findAllGroupsForAssignment(assignment);
if (full) { if (full) {
getLogger().debug({ full: full, groups: groups });
return groups.map(mapToGroupDTO); return groups.map(mapToGroupDTO);
} }
return groups.map(mapToGroupDTOId); return groups.map(mapToShallowGroupDTO);
} }
export async function getGroupSubmissions( export async function getGroupSubmissions(
@ -112,26 +97,7 @@ export async function getGroupSubmissions(
groupNumber: number, groupNumber: number,
full: boolean full: boolean
): Promise<SubmissionDTO[] | SubmissionDTOId[]> { ): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const classRepository = getClassRepository(); const group = await fetchGroup(classId, assignmentNumber, groupNumber);
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 submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findAllSubmissionsForGroup(group); const submissions = await submissionRepository.findAllSubmissionsForGroup(group);

View file

@ -1,10 +1,10 @@
import { getAttachmentRepository } from '../../data/repositories.js'; import { getAttachmentRepository } from '../../data/repositories.js';
import { Attachment } from '../../entities/content/attachment.entity.js'; import { Attachment } from '../../entities/content/attachment.entity.js';
import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content';
const attachmentService = { const attachmentService = {
async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> { async getAttachment(learningObjectId: LearningObjectIdentifierDTO, attachmentName: string): Promise<Attachment | null> {
const attachmentRepo = getAttachmentRepository(); const attachmentRepo = getAttachmentRepository();
if (learningObjectId.version) { if (learningObjectId.version) {

View file

@ -6,7 +6,7 @@ import processingService from './processing/processing-service.js';
import { NotFoundError } from '@mikro-orm/core'; import { NotFoundError } from '@mikro-orm/core';
import learningObjectService from './learning-object-service.js'; import learningObjectService from './learning-object-service.js';
import { getLogger, Logger } from '../../logging/initalize.js'; import { getLogger, Logger } from '../../logging/initalize.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
const logger: Logger = getLogger(); const logger: Logger = getLogger();
@ -40,7 +40,7 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
}; };
} }
async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> { async function findLearningObjectEntityById(id: LearningObjectIdentifierDTO): Promise<LearningObject | null> {
const learningObjectRepo = getLearningObjectRepository(); const learningObjectRepo = getLearningObjectRepository();
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
@ -53,7 +53,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
/** /**
* Fetches a single learning object by its HRUID * Fetches a single learning object by its HRUID
*/ */
async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> {
const learningObject = await findLearningObjectEntityById(id); const learningObject = await findLearningObjectEntityById(id);
return convertLearningObject(learningObject); return convertLearningObject(learningObject);
}, },
@ -61,7 +61,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
/** /**
* Obtain a HTML-rendering of the learning object with the given identifier (as a string). * Obtain a HTML-rendering of the learning object with the given identifier (as a string).
*/ */
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> {
const learningObjectRepo = getLearningObjectRepository(); const learningObjectRepo = getLearningObjectRepository();
const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);

View file

@ -5,7 +5,7 @@ import { LearningObjectProvider } from './learning-object-provider.js';
import { getLogger, Logger } from '../../logging/initalize.js'; import { getLogger, Logger } from '../../logging/initalize.js';
import { import {
FilteredLearningObject, FilteredLearningObject,
LearningObjectIdentifier, LearningObjectIdentifierDTO,
LearningObjectMetadata, LearningObjectMetadata,
LearningObjectNode, LearningObjectNode,
LearningPathIdentifier, LearningPathIdentifier,
@ -67,7 +67,7 @@ async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full
const objects = await Promise.all( const objects = await Promise.all(
nodes.map(async (node) => { nodes.map(async (node) => {
const learningObjectId: LearningObjectIdentifier = { const learningObjectId: LearningObjectIdentifierDTO = {
hruid: node.learningobject_hruid, hruid: node.learningobject_hruid,
language: learningPathId.language, language: learningPathId.language,
}; };
@ -85,7 +85,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
/** /**
* Fetches a single learning object by its HRUID * Fetches a single learning object by its HRUID
*/ */
async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> {
const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`;
const metadata = await fetchWithLogging<LearningObjectMetadata>( const metadata = await fetchWithLogging<LearningObjectMetadata>(
metadataUrl, metadataUrl,
@ -121,7 +121,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
* Obtain a HTML-rendering of the learning object with the given identifier (as a string). For learning objects * Obtain a HTML-rendering of the learning object with the given identifier (as a string). For learning objects
* from the Dwengo API, this means passing through the HTML rendering from there. * from the Dwengo API, this means passing through the HTML rendering from there.
*/ */
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> {
const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`;
const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, {
params: { ...id }, params: { ...id },

View file

@ -1,10 +1,10 @@
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
export interface LearningObjectProvider { export interface LearningObjectProvider {
/** /**
* Fetches a single learning object by its HRUID * Fetches a single learning object by its HRUID
*/ */
getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null>; getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null>;
/** /**
* Fetch full learning object data (metadata) * Fetch full learning object data (metadata)
@ -19,5 +19,5 @@ export interface LearningObjectProvider {
/** /**
* Obtain a HTML-rendering of the learning object with the given identifier (as a string). * Obtain a HTML-rendering of the learning object with the given identifier (as a string).
*/ */
getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null>; getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null>;
} }

View file

@ -2,9 +2,9 @@ import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provid
import { LearningObjectProvider } from './learning-object-provider.js'; import { LearningObjectProvider } from './learning-object-provider.js';
import { envVars, getEnvVar } from '../../util/envVars.js'; import { envVars, getEnvVar } from '../../util/envVars.js';
import databaseLearningObjectProvider from './database-learning-object-provider.js'; import databaseLearningObjectProvider from './database-learning-object-provider.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider {
if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
return databaseLearningObjectProvider; return databaseLearningObjectProvider;
} }
@ -18,7 +18,7 @@ const learningObjectService = {
/** /**
* Fetches a single learning object by its HRUID * Fetches a single learning object by its HRUID
*/ */
async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> {
return getProvider(id).getLearningObjectById(id); return getProvider(id).getLearningObjectById(id);
}, },
@ -39,7 +39,7 @@ const learningObjectService = {
/** /**
* Obtain a HTML-rendering of the learning object with the given identifier (as a string). * Obtain a HTML-rendering of the learning object with the given identifier (as a string).
*/ */
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> {
return getProvider(id).getLearningObjectHTML(id); return getProvider(id).getLearningObjectHTML(id);
}, },
}; };

View file

@ -12,7 +12,7 @@ import Image = marked.Tokens.Image;
import Heading = marked.Tokens.Heading; import Heading = marked.Tokens.Heading;
import Link = marked.Tokens.Link; import Link = marked.Tokens.Link;
import RendererObject = marked.RendererObject; import RendererObject = marked.RendererObject;
import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
const prefixes = { const prefixes = {
@ -25,7 +25,7 @@ const prefixes = {
blockly: '@blockly', blockly: '@blockly',
}; };
function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifier { function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifierDTO {
const [hruid, language, version] = href.split(/\/(.+)/, 2)[1].split('/'); const [hruid, language, version] = href.split(/\/(.+)/, 2)[1].split('/');
return { return {
hruid, hruid,

View file

@ -14,7 +14,7 @@ import { LearningObject } from '../../../entities/content/learning-object.entity
import Processor from './processor.js'; import Processor from './processor.js';
import { DwengoContentType } from './content-type.js'; import { DwengoContentType } from './content-type.js';
import { replaceAsync } from '../../../util/async.js'; import { replaceAsync } from '../../../util/async.js';
import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" language="([^"]+)" version="([^"]+)"\/>/g; const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" language="([^"]+)" version="([^"]+)"\/>/g;
@ -50,7 +50,7 @@ class ProcessingService {
*/ */
async render( async render(
learningObject: LearningObject, learningObject: LearningObject,
fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifier) => Promise<LearningObject | null> fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifierDTO) => Promise<LearningObject | null>
): Promise<string> { ): Promise<string> {
const html = this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject); const html = this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject);
if (fetchEmbeddedLearningObjects) { if (fetchEmbeddedLearningObjects) {

View file

@ -1,22 +1,39 @@
import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js'; import { getAnswerRepository, getAssignmentRepository, getClassRepository, getGroupRepository, getQuestionRepository } from '../data/repositories.js';
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; import { mapToLearningObjectID, mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
import { Question } from '../entities/questions/question.entity.js'; import { Question } from '../entities/questions/question.entity.js';
import { Answer } from '../entities/questions/answer.entity.js'; import { Answer } from '../entities/questions/answer.entity.js';
import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js'; import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js';
import { QuestionRepository } from '../data/questions/question-repository.js'; import { QuestionRepository } from '../data/questions/question-repository.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToStudent } from '../interfaces/student.js'; import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
import { mapToAssignment } from '../interfaces/assignment.js';
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
import { fetchStudent } from './students.js';
import { NotFoundException } from '../exceptions/not-found-exception';
import { FALLBACK_VERSION_NUM } from '../config.js';
export async function getQuestionsAboutLearningObjectInAssignment(
loId: LearningObjectIdentifier,
classId: string,
assignmentId: number,
full: boolean,
studentUsername?: string
): Promise<QuestionDTO[] | QuestionId[]> {
const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, assignmentId);
const questions = await getQuestionRepository().findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, studentUsername);
if (full) {
return questions.map((q) => mapToQuestionDTO(q));
}
return questions.map((q) => mapToQuestionDTOId(q));
}
export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> { export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const questionRepository: QuestionRepository = getQuestionRepository(); const questionRepository: QuestionRepository = getQuestionRepository();
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
if (!questions) {
return [];
}
if (full) { if (full) {
return questions.map(mapToQuestionDTO); return questions.map(mapToQuestionDTO);
} }
@ -24,24 +41,22 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea
return questions.map(mapToQuestionDTOId); return questions.map(mapToQuestionDTOId);
} }
async function fetchQuestion(questionId: QuestionId): Promise<Question | null> { export async function fetchQuestion(questionId: QuestionId): Promise<Question> {
const questionRepository = getQuestionRepository(); const questionRepository = getQuestionRepository();
const question = await questionRepository.findByLearningObjectAndSequenceNumber(
return await questionRepository.findOne({ mapToLearningObjectID(questionId.learningObjectIdentifier),
learningObjectHruid: questionId.learningObjectIdentifier.hruid, questionId.sequenceNumber
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) { if (!question) {
return null; throw new NotFoundException('Question with loID and sequence number not found');
} }
return question;
}
export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO> {
const question = await fetchQuestion(questionId);
return mapToQuestionDTO(question); return mapToQuestionDTO(question);
} }
@ -66,48 +81,43 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean
return answers.map(mapToAnswerDTOId); return answers.map(mapToAnswerDTOId);
} }
export async function createQuestion(questionDTO: QuestionDTO): Promise<QuestionDTO | null> { export async function createQuestion(loId: LearningObjectIdentifier, questionData: QuestionData): Promise<QuestionDTO> {
const questionRepository = getQuestionRepository(); const questionRepository = getQuestionRepository();
const author = await fetchStudent(questionData.author!);
const content = questionData.content;
const author = mapToStudent(questionDTO.author); const clazz = await getClassRepository().findById((questionData.inGroup.assignment as AssignmentDTO).within);
const assignment = mapToAssignment(questionData.inGroup.assignment as AssignmentDTO, clazz!);
const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber);
const loId: LearningObjectIdentifier = { const question = await questionRepository.createQuestion({
...questionDTO.learningObjectIdentifier, loId,
version: questionDTO.learningObjectIdentifier.version ?? 1, author,
}; inGroup: inGroup!,
content,
try { });
await questionRepository.createQuestion({
loId,
author,
content: questionDTO.content,
});
} catch (_) {
return null;
}
return questionDTO;
}
export async function deleteQuestion(questionId: QuestionId): Promise<QuestionDTO | null> {
const questionRepository = getQuestionRepository();
const question = await fetchQuestion(questionId);
if (!question) {
return null;
}
const loId: LearningObjectIdentifier = {
...questionId.learningObjectIdentifier,
version: questionId.learningObjectIdentifier.version ?? 1,
};
try {
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(loId, questionId.sequenceNumber);
} catch (_) {
return null;
}
return mapToQuestionDTO(question); return mapToQuestionDTO(question);
} }
export async function deleteQuestion(questionId: QuestionId): Promise<QuestionDTO> {
const questionRepository = getQuestionRepository();
const question = await fetchQuestion(questionId); // Throws error if not found
const loId: LearningObjectIdentifier = {
hruid: questionId.learningObjectIdentifier.hruid,
language: questionId.learningObjectIdentifier.language,
version: questionId.learningObjectIdentifier.version || FALLBACK_VERSION_NUM,
};
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(loId, questionId.sequenceNumber);
return mapToQuestionDTO(question);
}
export async function updateQuestion(questionId: QuestionId, questionData: QuestionData): Promise<QuestionDTO> {
const questionRepository = getQuestionRepository();
const question = await fetchQuestion(questionId);
const newQuestion = await questionRepository.updateContent(question, questionData.content);
return mapToQuestionDTO(newQuestion);
}

View file

@ -0,0 +1,20 @@
import { EntityDTO, FromEntityType } from '@mikro-orm/core';
import { DwengoEntityRepository } from '../data/dwengo-entity-repository';
/**
* Utility function to perform an PUT on an object.
*
* @param object The object that needs to be changed
* @param data The datafields and their values that will be updated
* @param repo The repository on which this action needs to be performed
*
* @returns Nothing.
*/
export async function putObject<T extends object>(
object: T,
data: Partial<EntityDTO<FromEntityType<T>>>,
repo: DwengoEntityRepository<T>
): Promise<void> {
repo.assign(object, data);
await repo.getEntityManager().flush();
}

View file

@ -7,7 +7,7 @@ import {
getSubmissionRepository, getSubmissionRepository,
} from '../data/repositories.js'; } from '../data/repositories.js';
import { mapToClassDTO } from '../interfaces/class.js'; import { mapToClassDTO } from '../interfaces/class.js';
import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; import { mapToGroupDTO, mapToShallowGroupDTO } from '../interfaces/group.js';
import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js'; import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
import { getAllAssignments } from './assignments.js'; import { getAllAssignments } from './assignments.js';
@ -23,6 +23,8 @@ import { GroupDTO } from '@dwengo-1/common/interfaces/group';
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
import { ConflictException } from '../exceptions/conflict-exception.js';
import { Submission } from '../entities/assignments/submission.entity';
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> { export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
@ -100,14 +102,15 @@ export async function getStudentGroups(username: string, full: boolean): Promise
return groups.map(mapToGroupDTO); return groups.map(mapToGroupDTO);
} }
return groups.map(mapToGroupDTOId); return groups.map(mapToShallowGroupDTO);
} }
export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> { export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const student = await fetchStudent(username); const student = await fetchStudent(username);
const submissionRepository = getSubmissionRepository(); const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findAllSubmissionsForStudent(student);
const submissions: Submission[] = await submissionRepository.findAllSubmissionsForStudent(student);
if (full) { if (full) {
return submissions.map(mapToSubmissionDTO); return submissions.map(mapToSubmissionDTO);
@ -135,6 +138,10 @@ export async function createClassJoinRequest(username: string, classId: string):
const student = await fetchStudent(username); // Throws error if student not found const student = await fetchStudent(username); // Throws error if student not found
const cls = await fetchClass(classId); const cls = await fetchClass(classId);
if (cls.students.contains(student)) {
throw new ConflictException('Student already in this class');
}
const request = mapToStudentRequest(student, cls); const request = mapToStudentRequest(student, cls);
await requestRepo.save(request, { preventOverwrite: true }); await requestRepo.save(request, { preventOverwrite: true });
return mapToStudentRequestDTO(request); return mapToStudentRequestDTO(request);

View file

@ -1,57 +1,71 @@
import { getSubmissionRepository } from '../data/repositories.js'; import { getAssignmentRepository, getSubmissionRepository } from '../data/repositories.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js'; import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js';
import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission';
import { fetchStudent } from './students.js';
import { getExistingGroupFromGroupDTO } from './groups.js';
import { Submission } from '../entities/assignments/submission.entity.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
export async function getSubmission( export async function fetchSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission> {
learningObjectHruid: string,
language: Language,
version: number,
submissionNumber: number
): Promise<SubmissionDTO | null> {
const loId = new LearningObjectIdentifier(learningObjectHruid, language, version);
const submissionRepository = getSubmissionRepository(); const submissionRepository = getSubmissionRepository();
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber); const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber);
if (!submission) { if (!submission) {
return null; throw new NotFoundException('Could not find submission');
} }
return mapToSubmissionDTO(submission);
}
export async function createSubmission(submissionDTO: SubmissionDTO): Promise<SubmissionDTO | null> {
const submissionRepository = getSubmissionRepository();
const submission = mapToSubmission(submissionDTO);
try {
const newSubmission = submissionRepository.create(submission);
await submissionRepository.save(newSubmission);
} catch (_) {
return null;
}
return mapToSubmissionDTO(submission);
}
export async function deleteSubmission(
learningObjectHruid: string,
language: Language,
version: number,
submissionNumber: number
): Promise<SubmissionDTO | null> {
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; return submission;
} }
export async function getSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise<SubmissionDTO> {
const submission = await fetchSubmission(loId, submissionNumber);
return mapToSubmissionDTO(submission);
}
export async function getAllSubmissions(loId: LearningObjectIdentifier): Promise<SubmissionDTO[]> {
const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findByLearningObject(loId);
return submissions.map(mapToSubmissionDTO);
}
export async function createSubmission(submissionDTO: SubmissionDTO): Promise<SubmissionDTO> {
const submitter = await fetchStudent(submissionDTO.submitter.username);
const group = await getExistingGroupFromGroupDTO(submissionDTO.group);
const submissionRepository = getSubmissionRepository();
const submission = mapToSubmission(submissionDTO, submitter, group);
await submissionRepository.save(submission);
return mapToSubmissionDTO(submission);
}
export async function deleteSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise<SubmissionDTO> {
const submission = await fetchSubmission(loId, submissionNumber);
const submissionRepository = getSubmissionRepository();
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber);
return mapToSubmissionDTO(submission);
}
/**
* Returns all the submissions made by on behalf of any group the given student is in.
*/
export async function getSubmissionsForLearningObjectAndAssignment(
learningObjectHruid: string,
language: Language,
version: number,
classId: string,
assignmentId: number,
studentUsername?: string
): Promise<SubmissionDTO[]> {
const loId = new LearningObjectIdentifier(learningObjectHruid, language, version);
const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, assignmentId);
const submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, studentUsername);
return submissions.map((s) => mapToSubmissionDTO(s));
}

View file

@ -22,13 +22,14 @@ import { Question } from '../entities/questions/question.entity.js';
import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js'; import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js';
import { Student } from '../entities/users/student.entity.js'; import { Student } from '../entities/users/student.entity.js';
import { NotFoundException } from '../exceptions/not-found-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js';
import { getClassStudents } from './classes.js'; import { addClassStudent, fetchClass, getClassStudentsDTO } from './classes.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import { ClassDTO } from '@dwengo-1/common/interfaces/class'; import { ClassDTO } from '@dwengo-1/common/interfaces/class';
import { StudentDTO } from '@dwengo-1/common/interfaces/student'; import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request';
import { ConflictException } from '../exceptions/conflict-exception.js';
export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> { export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
const teacherRepository: TeacherRepository = getTeacherRepository(); const teacherRepository: TeacherRepository = getTeacherRepository();
@ -99,10 +100,12 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro
const classIds: string[] = classes.map((cls) => cls.id); const classIds: string[] = classes.map((cls) => cls.id);
const students: StudentDTO[] = (await Promise.all(classIds.map(async (id) => getClassStudents(id)))).flat(); const students: StudentDTO[] = (await Promise.all(classIds.map(async (username) => await getClassStudentsDTO(username)))).flat();
if (full) { if (full) {
return students; return students;
} }
return students.map((student) => student.username); return students.map((student) => student.username);
} }
@ -143,13 +146,12 @@ export async function getJoinRequestsByClass(classId: string): Promise<ClassJoin
export async function updateClassJoinRequestStatus(studentUsername: string, classId: string, accepted = true): Promise<ClassJoinRequestDTO> { export async function updateClassJoinRequestStatus(studentUsername: string, classId: string, accepted = true): Promise<ClassJoinRequestDTO> {
const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository(); const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository();
const classRepo: ClassRepository = getClassRepository();
const student: Student = await fetchStudent(studentUsername); const student: Student = await fetchStudent(studentUsername);
const cls: Class | null = await classRepo.findById(classId); const cls = await fetchClass(classId);
if (!cls) { if (cls.students.contains(student)) {
throw new NotFoundException('Class not found'); throw new ConflictException('Student already in this class');
} }
const request: ClassJoinRequest | null = await requestRepo.findByStudentAndClass(student, cls); const request: ClassJoinRequest | null = await requestRepo.findByStudentAndClass(student, cls);
@ -158,8 +160,14 @@ export async function updateClassJoinRequestStatus(studentUsername: string, clas
throw new NotFoundException('Join request not found'); throw new NotFoundException('Join request not found');
} }
request.status = accepted ? ClassJoinRequestStatus.Accepted : ClassJoinRequestStatus.Declined; request.status = ClassJoinRequestStatus.Declined;
if (accepted) {
request.status = ClassJoinRequestStatus.Accepted;
await addClassStudent(classId, studentUsername);
}
await requestRepo.save(request); await requestRepo.save(request);
return mapToStudentRequestDTO(request); return mapToStudentRequestDTO(request);
} }

View file

@ -1,4 +1,4 @@
import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content';
export function isValidHttpUrl(url: string): boolean { export function isValidHttpUrl(url: string): boolean {
try { try {
@ -9,7 +9,7 @@ export function isValidHttpUrl(url: string): boolean {
} }
} }
export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifier): string { export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifierDTO): string {
let url = `/learningObject/${learningObjectId.hruid}/html?language=${learningObjectId.language}`; let url = `/learningObject/${learningObjectId.hruid}/html?language=${learningObjectId.language}`;
if (learningObjectId.version) { if (learningObjectId.version) {
url += `&version=${learningObjectId.version}`; url += `&version=${learningObjectId.version}`;
@ -17,7 +17,7 @@ export function getUrlStringForLearningObject(learningObjectId: LearningObjectId
return url; return url;
} }
export function getUrlStringForLearningObjectHTML(learningObjectIdentifier: LearningObjectIdentifier): string { export function getUrlStringForLearningObjectHTML(learningObjectIdentifier: LearningObjectIdentifierDTO): string {
let url = `/learningObject/${learningObjectIdentifier.hruid}/html?language=${learningObjectIdentifier.language}`; let url = `/learningObject/${learningObjectIdentifier.hruid}/html?language=${learningObjectIdentifier.language}`;
if (learningObjectIdentifier.version) { if (learningObjectIdentifier.version) {
url += `&version=${learningObjectIdentifier.version}`; url += `&version=${learningObjectIdentifier.version}`;

View file

@ -0,0 +1,87 @@
import { Request, Response } from 'express';
import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { setupTestApp } from '../setup-tests';
import { Language } from '@dwengo-1/common/util/language';
import { getAllAnswersHandler, getAnswerHandler, updateAnswerHandler } from '../../src/controllers/answers';
import { BadRequestException } from '../../src/exceptions/bad-request-exception';
import { NotFoundException } from '../../src/exceptions/not-found-exception';
describe('Questions controllers', () => {
let req: Partial<Request>;
let res: Partial<Response>;
let jsonMock: Mock;
beforeAll(async () => {
await setupTestApp();
});
beforeEach(() => {
jsonMock = vi.fn();
res = {
json: jsonMock,
};
});
it('Get answers list', async () => {
req = {
params: { hruid: 'id05', version: '1', seq: '2' },
query: { lang: Language.English, full: 'true' },
};
await getAllAnswersHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ answers: expect.anything() }));
const result = jsonMock.mock.lastCall?.[0];
// Console.log(result.answers);
expect(result.answers).to.have.length.greaterThan(1);
});
it('Get answer', async () => {
req = {
params: { hruid: 'id05', version: '1', seq: '2', seqAnswer: '2' },
query: { lang: Language.English, full: 'true' },
};
await getAnswerHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ answer: expect.anything() }));
// Const result = jsonMock.mock.lastCall?.[0];
// Console.log(result.answer);
});
it('Get answer hruid does not exist', async () => {
req = {
params: { hruid: 'id_not_exist' },
query: { lang: Language.English, full: 'true' },
};
await expect(async () => getAnswerHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
});
it('Get answer no hruid given', async () => {
req = {
params: {},
query: { lang: Language.English, full: 'true' },
};
await expect(async () => getAnswerHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException);
});
it('Update question', async () => {
const newContent = 'updated question';
req = {
params: { hruid: 'id05', version: '1', seq: '2', seqAnswer: '2' },
query: { lang: Language.English },
body: { content: newContent },
};
await updateAnswerHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ answer: expect.anything() }));
const result = jsonMock.mock.lastCall?.[0];
// Console.log(result.question);
expect(result.answer.content).to.eq(newContent);
});
});

View file

@ -0,0 +1,117 @@
import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { Request, Response } from 'express';
import { setupTestApp } from '../setup-tests';
import { getAllQuestionsHandler, getQuestionHandler, updateQuestionHandler } from '../../src/controllers/questions';
import { Language } from '@dwengo-1/common/util/language';
import { NotFoundException } from '../../src/exceptions/not-found-exception';
import { BadRequestException } from '../../src/exceptions/bad-request-exception';
describe('Questions controllers', () => {
let req: Partial<Request>;
let res: Partial<Response>;
let jsonMock: Mock;
beforeAll(async () => {
await setupTestApp();
});
beforeEach(() => {
jsonMock = vi.fn();
res = {
json: jsonMock,
};
});
it('Get question list', async () => {
req = {
params: { hruid: 'id05', version: '1' },
query: { lang: Language.English, full: 'true' },
};
await getAllQuestionsHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ questions: expect.anything() }));
const result = jsonMock.mock.lastCall?.[0];
// Console.log(result.questions);
expect(result.questions).to.have.length.greaterThan(1);
});
it('Get question', async () => {
req = {
params: { hruid: 'id05', version: '1', seq: '1' },
query: { lang: Language.English, full: 'true' },
};
await getQuestionHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() }));
// Const result = jsonMock.mock.lastCall?.[0];
// Console.log(result.question);
});
it('Get question with fallback sequence number and version', async () => {
req = {
params: { hruid: 'id05' },
query: { lang: Language.English, full: 'true' },
};
await getQuestionHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() }));
// Const result = jsonMock.mock.lastCall?.[0];
// Console.log(result.question);
});
it('Get question hruid does not exist', async () => {
req = {
params: { hruid: 'id_not_exist' },
query: { lang: Language.English, full: 'true' },
};
await expect(async () => getQuestionHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
});
it('Get question no hruid given', async () => {
req = {
params: {},
query: { lang: Language.English, full: 'true' },
};
await expect(async () => getQuestionHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException);
});
/*
It('Create and delete question', async() => {
req = {
params: { hruid: 'id05', version: '1', seq: '2'},
query: { lang: Language.English },
};
await deleteQuestionHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() }));
const result = jsonMock.mock.lastCall?.[0];
console.log(result.question);
});
*/
it('Update question', async () => {
const newContent = 'updated question';
req = {
params: { hruid: 'id05', version: '1', seq: '1' },
query: { lang: Language.English },
body: { content: newContent },
};
await updateQuestionHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() }));
const result = jsonMock.mock.lastCall?.[0];
// Console.log(result.question);
expect(result.question.content).to.eq(newContent);
});
});

View file

@ -186,7 +186,7 @@ describe('Student controllers', () => {
it('Get join request by student and class', async () => { it('Get join request by student and class', async () => {
req = { req = {
params: { username: 'PinkFloyd', classId: 'id02' }, params: { username: 'PinkFloyd', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
}; };
await getStudentRequestHandler(req as Request, res as Response); await getStudentRequestHandler(req as Request, res as Response);
@ -198,29 +198,18 @@ describe('Student controllers', () => {
); );
}); });
it('Create join request', async () => { it('Create and delete join request', async () => {
req = { req = {
params: { username: 'Noordkaap' }, params: { username: 'TheDoors' },
body: { classId: 'id02' }, body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
}; };
await createStudentRequestHandler(req as Request, res as Response); await createStudentRequestHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() }));
});
it('Create join request duplicate', async () => {
req = { req = {
params: { username: 'Tool' }, params: { username: 'TheDoors', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
body: { classId: 'id02' },
};
await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException);
});
it('Delete join request', async () => {
req = {
params: { username: 'Noordkaap', classId: 'id02' },
}; };
await deleteClassJoinRequestHandler(req as Request, res as Response); await deleteClassJoinRequestHandler(req as Request, res as Response);
@ -229,4 +218,22 @@ describe('Student controllers', () => {
await expect(async () => deleteClassJoinRequestHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); await expect(async () => deleteClassJoinRequestHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
}); });
it('Create join request student already in class error', async () => {
req = {
params: { username: 'Noordkaap' },
body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
};
await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException);
});
it('Create join request duplicate', async () => {
req = {
params: { username: 'Tool' },
body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
};
await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException);
});
}); });

View file

@ -16,6 +16,7 @@ import { BadRequestException } from '../../src/exceptions/bad-request-exception.
import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js';
import { getStudentRequestsHandler } from '../../src/controllers/students.js'; import { getStudentRequestsHandler } from '../../src/controllers/students.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import { getClassHandler } from '../../src/controllers/classes';
describe('Teacher controllers', () => { describe('Teacher controllers', () => {
let req: Partial<Request>; let req: Partial<Request>;
@ -104,9 +105,9 @@ describe('Teacher controllers', () => {
const result = jsonMock.mock.lastCall?.[0]; const result = jsonMock.mock.lastCall?.[0];
const teacherUsernames = result.teachers.map((s: TeacherDTO) => s.username); const teacherUsernames = result.teachers.map((s: TeacherDTO) => s.username);
expect(teacherUsernames).toContain('FooFighters'); expect(teacherUsernames).toContain('testleerkracht1');
expect(result.teachers).toHaveLength(4); expect(result.teachers).toHaveLength(5);
}); });
it('Deleting non-existent student', async () => { it('Deleting non-existent student', async () => {
@ -117,7 +118,7 @@ describe('Teacher controllers', () => {
it('Get teacher classes', async () => { it('Get teacher classes', async () => {
req = { req = {
params: { username: 'FooFighters' }, params: { username: 'testleerkracht1' },
query: { full: 'true' }, query: { full: 'true' },
}; };
@ -132,7 +133,7 @@ describe('Teacher controllers', () => {
it('Get teacher students', async () => { it('Get teacher students', async () => {
req = { req = {
params: { username: 'FooFighters' }, params: { username: 'testleerkracht1' },
query: { full: 'true' }, query: { full: 'true' },
}; };
@ -168,8 +169,7 @@ describe('Teacher controllers', () => {
it('Get join requests by class', async () => { it('Get join requests by class', async () => {
req = { req = {
query: { username: 'LimpBizkit' }, params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
params: { classId: 'id02' },
}; };
await getStudentJoinRequestHandler(req as Request, res as Response); await getStudentJoinRequestHandler(req as Request, res as Response);
@ -183,8 +183,7 @@ describe('Teacher controllers', () => {
it('Update join request status', async () => { it('Update join request status', async () => {
req = { req = {
query: { username: 'LimpBizkit', studentUsername: 'PinkFloyd' }, params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', studentUsername: 'PinkFloyd' },
params: { classId: 'id02' },
body: { accepted: 'true' }, body: { accepted: 'true' },
}; };
@ -200,5 +199,13 @@ describe('Teacher controllers', () => {
const status: boolean = jsonMock.mock.lastCall?.[0].requests[0].status; const status: boolean = jsonMock.mock.lastCall?.[0].requests[0].status;
expect(status).toBeTruthy(); expect(status).toBeTruthy();
req = {
params: { id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
};
await getClassHandler(req as Request, res as Response);
const students: string[] = jsonMock.mock.lastCall?.[0].class.students;
expect(students).contains('PinkFloyd');
}); });
}); });

View file

@ -15,7 +15,7 @@ describe('AssignmentRepository', () => {
}); });
it('should return the requested assignment', async () => { it('should return the requested assignment', async () => {
const class_ = await classRepository.findById('id02'); const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
const assignment = await assignmentRepository.findByClassAndId(class_!, 2); const assignment = await assignmentRepository.findByClassAndId(class_!, 2);
expect(assignment).toBeTruthy(); expect(assignment).toBeTruthy();
@ -23,7 +23,7 @@ describe('AssignmentRepository', () => {
}); });
it('should return all assignments for a class', async () => { it('should return all assignments for a class', async () => {
const class_ = await classRepository.findById('id02'); const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
const assignments = await assignmentRepository.findAllAssignmentsInClass(class_!); const assignments = await assignmentRepository.findAllAssignmentsInClass(class_!);
expect(assignments).toBeTruthy(); expect(assignments).toBeTruthy();
@ -31,6 +31,13 @@ describe('AssignmentRepository', () => {
expect(assignments[0].title).toBe('tool'); expect(assignments[0].title).toBe('tool');
}); });
it('should find all by username of the responsible teacher', async () => {
const result = await assignmentRepository.findAllByResponsibleTeacher('testleerkracht1');
const resultIds = result.map((it) => it.id).sort((a, b) => (a ?? 0) - (b ?? 0));
expect(resultIds).toEqual([1, 3, 4]);
});
it('should not find removed assignment', async () => { it('should not find removed assignment', async () => {
const class_ = await classRepository.findById('id01'); const class_ = await classRepository.findById('id01');
await assignmentRepository.deleteByClassAndId(class_!, 3); await assignmentRepository.deleteByClassAndId(class_!, 3);

View file

@ -18,7 +18,7 @@ describe('GroupRepository', () => {
}); });
it('should return the requested group', async () => { it('should return the requested group', async () => {
const class_ = await classRepository.findById('id01'); const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
const assignment = await assignmentRepository.findByClassAndId(class_!, 1); const assignment = await assignmentRepository.findByClassAndId(class_!, 1);
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1);
@ -27,7 +27,7 @@ describe('GroupRepository', () => {
}); });
it('should return all groups for assignment', async () => { it('should return all groups for assignment', async () => {
const class_ = await classRepository.findById('id01'); const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
const assignment = await assignmentRepository.findByClassAndId(class_!, 1); const assignment = await assignmentRepository.findByClassAndId(class_!, 1);
const groups = await groupRepository.findAllGroupsForAssignment(assignment!); const groups = await groupRepository.findAllGroupsForAssignment(assignment!);
@ -37,7 +37,7 @@ describe('GroupRepository', () => {
}); });
it('should not find removed group', async () => { it('should not find removed group', async () => {
const class_ = await classRepository.findById('id02'); const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
const assignment = await assignmentRepository.findByClassAndId(class_!, 2); const assignment = await assignmentRepository.findByClassAndId(class_!, 2);
await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, 1); await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, 1);

View file

@ -14,6 +14,9 @@ import { StudentRepository } from '../../../src/data/users/student-repository';
import { GroupRepository } from '../../../src/data/assignments/group-repository'; import { GroupRepository } from '../../../src/data/assignments/group-repository';
import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository'; import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository';
import { ClassRepository } from '../../../src/data/classes/class-repository'; import { ClassRepository } from '../../../src/data/classes/class-repository';
import { Submission } from '../../../src/entities/assignments/submission.entity';
import { Class } from '../../../src/entities/classes/class.entity';
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
describe('SubmissionRepository', () => { describe('SubmissionRepository', () => {
let submissionRepository: SubmissionRepository; let submissionRepository: SubmissionRepository;
@ -50,7 +53,7 @@ describe('SubmissionRepository', () => {
it('should find the most recent submission for a group', async () => { 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 class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
const assignment = await assignmentRepository.findByClassAndId(class_!, 1); const assignment = await assignmentRepository.findByClassAndId(class_!, 1);
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1);
const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, group!); const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, group!);
@ -59,6 +62,49 @@ describe('SubmissionRepository', () => {
expect(submission?.submissionTime.getDate()).toBe(25); expect(submission?.submissionTime.getDate()).toBe(25);
}); });
let clazz: Class | null;
let assignment: Assignment | null;
let loId: LearningObjectIdentifier;
it('should find all submissions for a certain learning object and assignment', async () => {
clazz = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
assignment = await assignmentRepository.findByClassAndId(clazz!, 1);
loId = {
hruid: 'id02',
language: Language.English,
version: 1,
};
const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!);
sortSubmissions(result);
expect(result).toHaveLength(3);
// Submission3 should be found (for learning object 'id02' by group #1 for Assignment #1 in class 'id01')
expect(result[0].learningObjectHruid).toBe(loId.hruid);
expect(result[0].submissionNumber).toBe(1);
// Submission4 should be found (for learning object 'id02' by group #1 for Assignment #1 in class 'id01')
expect(result[1].learningObjectHruid).toBe(loId.hruid);
expect(result[1].submissionNumber).toBe(2);
// Submission8 should be found (for learning object 'id02' by group #2 for Assignment #1 in class 'id01')
expect(result[2].learningObjectHruid).toBe(loId.hruid);
expect(result[2].submissionNumber).toBe(3);
});
it("should find only the submissions for a certain learning object and assignment made for the user's group", async () => {
const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, 'Tool');
// (student Tool is in group #2)
expect(result).toHaveLength(1);
// Submission8 should be found (for learning object 'id02' by group #2 for Assignment #1 in class 'id01')
expect(result[0].learningObjectHruid).toBe(loId.hruid);
expect(result[0].submissionNumber).toBe(3);
// The other submissions found in the previous test case should not be found anymore as they were made on
// Behalf of group #1 which Tool is no member of.
});
it('should not find a deleted submission', async () => { 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); await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1);
@ -68,3 +114,15 @@ describe('SubmissionRepository', () => {
expect(submission).toBeNull(); expect(submission).toBeNull();
}); });
}); });
function sortSubmissions(submissions: Submission[]): void {
submissions.sort((a, b) => {
if (a.learningObjectHruid < b.learningObjectHruid) {
return -1;
}
if (a.learningObjectHruid > b.learningObjectHruid) {
return 1;
}
return a.submissionNumber! - b.submissionNumber!;
});
}

View file

@ -26,7 +26,7 @@ describe('ClassJoinRequestRepository', () => {
}); });
it('should list all requests to a single class', async () => { it('should list all requests to a single class', async () => {
const class_ = await cassRepository.findById('id02'); const class_ = await cassRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_!); const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_!);
expect(requests).toBeTruthy(); expect(requests).toBeTruthy();
@ -35,7 +35,7 @@ describe('ClassJoinRequestRepository', () => {
it('should not find a removed request', async () => { it('should not find a removed request', async () => {
const student = await studentRepository.findByUsername('SmashingPumpkins'); const student = await studentRepository.findByUsername('SmashingPumpkins');
const class_ = await cassRepository.findById('id03'); const class_ = await cassRepository.findById('80dcc3e0-1811-4091-9361-42c0eee91cfa');
await classJoinRequestRepository.deleteBy(student!, class_!); await classJoinRequestRepository.deleteBy(student!, class_!);
const request = await classJoinRequestRepository.findAllRequestsBy(student!); const request = await classJoinRequestRepository.findAllRequestsBy(student!);

View file

@ -18,16 +18,16 @@ describe('ClassRepository', () => {
}); });
it('should return requested class', async () => { it('should return requested class', async () => {
const classVar = await classRepository.findById('id01'); const classVar = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
expect(classVar).toBeTruthy(); expect(classVar).toBeTruthy();
expect(classVar?.displayName).toBe('class01'); expect(classVar?.displayName).toBe('class01');
}); });
it('class should be gone after deletion', async () => { it('class should be gone after deletion', async () => {
await classRepository.deleteById('id04'); await classRepository.deleteById('33d03536-83b8-4880-9982-9bbf2f908ddf');
const classVar = await classRepository.findById('id04'); const classVar = await classRepository.findById('33d03536-83b8-4880-9982-9bbf2f908ddf');
expect(classVar).toBeNull(); expect(classVar).toBeNull();
}); });

View file

@ -34,7 +34,7 @@ describe('ClassRepository', () => {
}); });
it('should return all invitations for a class', async () => { it('should return all invitations for a class', async () => {
const class_ = await classRepository.findById('id02'); const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_!); const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_!);
expect(invitations).toBeTruthy(); expect(invitations).toBeTruthy();
@ -42,7 +42,7 @@ describe('ClassRepository', () => {
}); });
it('should not find a removed invitation', async () => { it('should not find a removed invitation', async () => {
const class_ = await classRepository.findById('id01'); const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
const sender = await teacherRepository.findByUsername('FooFighters'); const sender = await teacherRepository.findByUsername('FooFighters');
const receiver = await teacherRepository.findByUsername('LimpBizkit'); const receiver = await teacherRepository.findByUsername('LimpBizkit');
await teacherInvitationRepository.deleteBy(class_!, sender!, receiver!); await teacherInvitationRepository.deleteBy(class_!, sender!, receiver!);

View file

@ -1,10 +1,19 @@
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import { QuestionRepository } from '../../../src/data/questions/question-repository'; import { QuestionRepository } from '../../../src/data/questions/question-repository';
import { getQuestionRepository, getStudentRepository } from '../../../src/data/repositories'; import {
getAssignmentRepository,
getClassRepository,
getGroupRepository,
getQuestionRepository,
getStudentRepository,
} from '../../../src/data/repositories';
import { StudentRepository } from '../../../src/data/users/student-repository'; import { StudentRepository } from '../../../src/data/users/student-repository';
import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { Question } from '../../../src/entities/questions/question.entity';
import { Class } from '../../../src/entities/classes/class.entity';
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
describe('QuestionRepository', () => { describe('QuestionRepository', () => {
let questionRepository: QuestionRepository; let questionRepository: QuestionRepository;
@ -21,14 +30,19 @@ describe('QuestionRepository', () => {
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
expect(questions).toBeTruthy(); expect(questions).toBeTruthy();
expect(questions).toHaveLength(2); expect(questions).toHaveLength(4);
}); });
it('should create new question', async () => { 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'); const student = await studentRepository.findByUsername('Noordkaap');
const clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 1);
const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 1);
await questionRepository.createQuestion({ await questionRepository.createQuestion({
loId: id, loId: id,
inGroup: group!,
author: student!, author: student!,
content: 'question?', content: 'question?',
}); });
@ -38,6 +52,52 @@ describe('QuestionRepository', () => {
expect(question).toHaveLength(1); expect(question).toHaveLength(1);
}); });
let clazz: Class | null;
let assignment: Assignment | null;
let loId: LearningObjectIdentifier;
it('should find all questions for a certain learning object and assignment', async () => {
clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
assignment = await getAssignmentRepository().findByClassAndId(clazz!, 1);
loId = {
hruid: 'id05',
language: Language.English,
version: 1,
};
const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!);
sortQuestions(result);
expect(result).toHaveLength(3);
// Question01: About learning object 'id05', in group #1 for Assignment #1 in class 'id01'
expect(result[0].learningObjectHruid).toEqual(loId.hruid);
expect(result[0].sequenceNumber).toEqual(1);
// Question02: About learning object 'id05', in group #1 for Assignment #1 in class 'id01'
expect(result[1].learningObjectHruid).toEqual(loId.hruid);
expect(result[1].sequenceNumber).toEqual(2);
// Question05: About learning object 'id05', in group #2 for Assignment #1 in class 'id01'
expect(result[2].learningObjectHruid).toEqual(loId.hruid);
expect(result[2].sequenceNumber).toEqual(3);
// Question06: About learning object 'id05', but for Assignment #2 in class 'id01' => not expected.
});
it("should find only the questions for a certain learning object and assignment asked by the user's group", async () => {
const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, 'Tool');
// (student Tool is in group #2)
expect(result).toHaveLength(1);
// Question01 and question02 are in group #1 => not displayed.
// Question05: About learning object 'id05', in group #2 for Assignment #1 in class 'id01'
expect(result[0].learningObjectHruid).toEqual(loId.hruid);
expect(result[0].sequenceNumber).toEqual(3);
// Question06: About learning object 'id05', but for Assignment #2 in class 'id01' => not expected.
});
it('should not find removed question', async () => { 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); await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(id, 1);
@ -47,3 +107,14 @@ describe('QuestionRepository', () => {
expect(question).toHaveLength(0); expect(question).toHaveLength(0);
}); });
}); });
function sortQuestions(questions: Question[]): void {
questions.sort((a, b) => {
if (a.learningObjectHruid < b.learningObjectHruid) {
return -1;
} else if (a.learningObjectHruid > b.learningObjectHruid) {
return 1;
}
return a.sequenceNumber! - b.sequenceNumber!;
});
}

View file

@ -7,11 +7,11 @@ import learningObjectService from '../../../src/services/learning-objects/learni
import { envVars, getEnvVar } from '../../../src/util/envVars'; import { envVars, getEnvVar } from '../../../src/util/envVars';
import { LearningPath } from '../../../src/entities/content/learning-path.entity'; import { LearningPath } from '../../../src/entities/content/learning-path.entity';
import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; import learningPathExample from '../../test-assets/learning-paths/pn-werking-example';
import { LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
const EXPECTED_DWENGO_LEARNING_OBJECT_TITLE = 'Werken met notebooks'; const EXPECTED_DWENGO_LEARNING_OBJECT_TITLE = 'Werken met notebooks';
const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifier = { const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifierDTO = {
hruid: 'pn_werkingnotebooks', hruid: 'pn_werkingnotebooks',
language: Language.Dutch, language: Language.Dutch,
version: 3, version: 3,

View file

@ -3,6 +3,9 @@ import { LearningObject } from '../../../src/entities/content/learning-object.en
import { setupTestApp } from '../../setup-tests.js'; import { setupTestApp } from '../../setup-tests.js';
import { LearningPath } from '../../../src/entities/content/learning-path.entity.js'; import { LearningPath } from '../../../src/entities/content/learning-path.entity.js';
import { import {
getAssignmentRepository,
getClassRepository,
getGroupRepository,
getLearningObjectRepository, getLearningObjectRepository,
getLearningPathRepository, getLearningPathRepository,
getStudentRepository, getStudentRepository,
@ -22,6 +25,10 @@ import { Student } from '../../../src/entities/users/student.entity.js';
import { LearningObjectNode, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectNode, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
const STUDENT_A_USERNAME = 'student_a';
const STUDENT_B_USERNAME = 'student_b';
const CLASS_NAME = 'test_class';
async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> {
const learningObjectRepo = getLearningObjectRepository(); const learningObjectRepo = getLearningObjectRepository();
const learningPathRepo = getLearningPathRepository(); const learningPathRepo = getLearningPathRepository();
@ -38,6 +45,9 @@ async function initPersonalizationTestData(): Promise<{
studentB: Student; studentB: Student;
}> { }> {
const studentRepo = getStudentRepository(); const studentRepo = getStudentRepository();
const classRepo = getClassRepository();
const assignmentRepo = getAssignmentRepository();
const groupRepo = getGroupRepository();
const submissionRepo = getSubmissionRepository(); const submissionRepo = getSubmissionRepository();
const learningPathRepo = getLearningPathRepository(); const learningPathRepo = getLearningPathRepository();
const learningObjectRepo = getLearningObjectRepository(); const learningObjectRepo = getLearningObjectRepository();
@ -47,32 +57,69 @@ async function initPersonalizationTestData(): Promise<{
await learningObjectRepo.save(learningContent.extraExerciseObject); await learningObjectRepo.save(learningContent.extraExerciseObject);
await learningPathRepo.save(learningContent.learningPath); await learningPathRepo.save(learningContent.learningPath);
// Create students
const studentA = studentRepo.create({ const studentA = studentRepo.create({
username: 'student_a', username: STUDENT_A_USERNAME,
firstName: 'Aron', firstName: 'Aron',
lastName: 'Student', lastName: 'Student',
}); });
await studentRepo.save(studentA); await studentRepo.save(studentA);
const studentB = studentRepo.create({
username: STUDENT_B_USERNAME,
firstName: 'Bill',
lastName: 'Student',
});
await studentRepo.save(studentB);
// Create class for students
const testClass = classRepo.create({
classId: CLASS_NAME,
displayName: 'Test class',
});
await classRepo.save(testClass);
// Create assignment for students and assign them to different groups
const assignment = assignmentRepo.create({
id: 0,
title: 'Test assignment',
description: 'Test description',
learningPathHruid: learningContent.learningPath.hruid,
learningPathLanguage: learningContent.learningPath.language,
within: testClass,
});
const groupA = groupRepo.create({
groupNumber: 0,
members: [studentA],
assignment,
});
await groupRepo.save(groupA);
const groupB = groupRepo.create({
groupNumber: 1,
members: [studentB],
assignment,
});
await groupRepo.save(groupB);
// Let each of the students make a submission in his own group.
const submissionA = submissionRepo.create({ const submissionA = submissionRepo.create({
learningObjectHruid: learningContent.branchingObject.hruid, learningObjectHruid: learningContent.branchingObject.hruid,
learningObjectLanguage: learningContent.branchingObject.language, learningObjectLanguage: learningContent.branchingObject.language,
learningObjectVersion: learningContent.branchingObject.version, learningObjectVersion: learningContent.branchingObject.version,
onBehalfOf: groupA,
submitter: studentA, submitter: studentA,
submissionTime: new Date(), submissionTime: new Date(),
content: '[0]', content: '[0]',
}); });
await submissionRepo.save(submissionA); await submissionRepo.save(submissionA);
const studentB = studentRepo.create({
username: 'student_b',
firstName: 'Bill',
lastName: 'Student',
});
await studentRepo.save(studentB);
const submissionB = submissionRepo.create({ const submissionB = submissionRepo.create({
learningObjectHruid: learningContent.branchingObject.hruid, learningObjectHruid: learningContent.branchingObject.hruid,
learningObjectLanguage: learningContent.branchingObject.language, learningObjectLanguage: learningContent.branchingObject.language,
learningObjectVersion: learningContent.branchingObject.version, learningObjectVersion: learningContent.branchingObject.version,
onBehalfOf: groupA,
submitter: studentB, submitter: studentB,
submissionTime: new Date(), submissionTime: new Date(),
content: '[1]', content: '[1]',

View file

@ -37,7 +37,7 @@ export async function setupTestApp(): Promise<void> {
learningObjects[1].attachments = attachments; learningObjects[1].attachments = attachments;
const questions = makeTestQuestions(em, students); const questions = makeTestQuestions(em, students, groups);
const answers = makeTestAnswers(em, teachers, questions); const answers = makeTestAnswers(em, teachers, questions);
const submissions = makeTestSubmissions(em, students, groups); const submissions = makeTestSubmissions(em, students, groups);

View file

@ -34,5 +34,15 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
groups: [], groups: [],
}); });
return [assignment01, assignment02, assignment03]; const assignment04 = em.create(Assignment, {
within: classes[0],
id: 4,
title: 'another assignment',
description: 'with a description',
learningPathHruid: 'id01',
learningPathLanguage: Language.English,
groups: [],
});
return [assignment01, assignment02, assignment03, assignment04];
} }

View file

@ -4,29 +4,55 @@ import { Assignment } from '../../../src/entities/assignments/assignment.entity'
import { Student } from '../../../src/entities/users/student.entity'; import { Student } from '../../../src/entities/users/student.entity';
export function makeTestGroups(em: EntityManager, students: Student[], assignments: Assignment[]): Group[] { export function makeTestGroups(em: EntityManager, students: Student[], assignments: Assignment[]): Group[] {
/*
* Group #1 for Assignment #1 in class 'id01'
* => Assigned to do learning path 'id02'
*/
const group01 = em.create(Group, { const group01 = em.create(Group, {
assignment: assignments[0], assignment: assignments[0],
groupNumber: 1, groupNumber: 1,
members: students.slice(0, 2), members: students.slice(0, 2),
}); });
/*
* Group #2 for Assignment #1 in class 'id01'
* => Assigned to do learning path 'id02'
*/
const group02 = em.create(Group, { const group02 = em.create(Group, {
assignment: assignments[0], assignment: assignments[0],
groupNumber: 2, groupNumber: 2,
members: students.slice(2, 4), members: students.slice(2, 4),
}); });
/*
* Group #3 for Assignment #1 in class 'id01'
* => Assigned to do learning path 'id02'
*/
const group03 = em.create(Group, { const group03 = em.create(Group, {
assignment: assignments[0], assignment: assignments[0],
groupNumber: 3, groupNumber: 3,
members: students.slice(4, 6), members: students.slice(4, 6),
}); });
/*
* Group #4 for Assignment #2 in class 'id02'
* => Assigned to do learning path 'id01'
*/
const group04 = em.create(Group, { const group04 = em.create(Group, {
assignment: assignments[1], assignment: assignments[1],
groupNumber: 4, groupNumber: 4,
members: students.slice(3, 4), members: students.slice(3, 4),
}); });
return [group01, group02, group03, group04]; /*
* Group #5 for Assignment #4 in class 'id01'
* => Assigned to do learning path 'id01'
*/
const group05 = em.create(Group, {
assignment: assignments[3],
groupNumber: 1,
members: students.slice(0, 2),
});
return [group01, group02, group03, group04, group05];
} }

View file

@ -12,7 +12,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou
submissionNumber: 1, submissionNumber: 1,
submitter: students[0], submitter: students[0],
submissionTime: new Date(2025, 2, 20), submissionTime: new Date(2025, 2, 20),
onBehalfOf: groups[0], onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01'
content: 'sub1', content: 'sub1',
}); });
@ -23,7 +23,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou
submissionNumber: 2, submissionNumber: 2,
submitter: students[0], submitter: students[0],
submissionTime: new Date(2025, 2, 25), submissionTime: new Date(2025, 2, 25),
onBehalfOf: groups[0], onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01'
content: '', content: '',
}); });
@ -34,6 +34,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou
submissionNumber: 1, submissionNumber: 1,
submitter: students[0], submitter: students[0],
submissionTime: new Date(2025, 2, 20), submissionTime: new Date(2025, 2, 20),
onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01'
content: '', content: '',
}); });
@ -44,6 +45,7 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou
submissionNumber: 2, submissionNumber: 2,
submitter: students[0], submitter: students[0],
submissionTime: new Date(2025, 2, 25), submissionTime: new Date(2025, 2, 25),
onBehalfOf: groups[0], // Group #1 for Assignment #1 in class 'id01'
content: '', content: '',
}); });
@ -54,8 +56,42 @@ export function makeTestSubmissions(em: EntityManager, students: Student[], grou
submissionNumber: 1, submissionNumber: 1,
submitter: students[1], submitter: students[1],
submissionTime: new Date(2025, 2, 20), submissionTime: new Date(2025, 2, 20),
onBehalfOf: groups[1], // Group #2 for Assignment #1 in class 'id01'
content: '', content: '',
}); });
return [submission01, submission02, submission03, submission04, submission05]; const submission06 = em.create(Submission, {
learningObjectHruid: 'id01',
learningObjectLanguage: Language.English,
learningObjectVersion: 1,
submissionNumber: 2,
submitter: students[1],
submissionTime: new Date(2025, 2, 25),
onBehalfOf: groups[4], // Group #5 for Assignment #4 in class 'id01'
content: '',
});
const submission07 = em.create(Submission, {
learningObjectHruid: 'id01',
learningObjectLanguage: Language.English,
learningObjectVersion: 1,
submissionNumber: 3,
submitter: students[3],
submissionTime: new Date(2025, 3, 25),
onBehalfOf: groups[3], // Group #4 for Assignment #2 in class 'id02'
content: '',
});
const submission08 = em.create(Submission, {
learningObjectHruid: 'id02',
learningObjectLanguage: Language.English,
learningObjectVersion: 1,
submissionNumber: 3,
submitter: students[1],
submissionTime: new Date(2025, 4, 7),
onBehalfOf: groups[1], // Group #2 for Assignment #1 in class 'id01'
content: '',
});
return [submission01, submission02, submission03, submission04, submission05, submission06, submission07, submission08];
} }

View file

@ -4,11 +4,11 @@ import { Student } from '../../../src/entities/users/student.entity';
import { Teacher } from '../../../src/entities/users/teacher.entity'; import { Teacher } from '../../../src/entities/users/teacher.entity';
export function makeTestClasses(em: EntityManager, students: Student[], teachers: Teacher[]): Class[] { export function makeTestClasses(em: EntityManager, students: Student[], teachers: Teacher[]): Class[] {
const studentsClass01 = students.slice(0, 7); const studentsClass01 = students.slice(0, 8);
const teacherClass01: Teacher[] = teachers.slice(0, 1); const teacherClass01: Teacher[] = teachers.slice(4, 5);
const class01 = em.create(Class, { const class01 = em.create(Class, {
classId: 'id01', classId: '8764b861-90a6-42e5-9732-c0d9eb2f55f9',
displayName: 'class01', displayName: 'class01',
teachers: teacherClass01, teachers: teacherClass01,
students: studentsClass01, students: studentsClass01,
@ -18,7 +18,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
const teacherClass02: Teacher[] = teachers.slice(1, 2); const teacherClass02: Teacher[] = teachers.slice(1, 2);
const class02 = em.create(Class, { const class02 = em.create(Class, {
classId: 'id02', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89',
displayName: 'class02', displayName: 'class02',
teachers: teacherClass02, teachers: teacherClass02,
students: studentsClass02, students: studentsClass02,
@ -28,7 +28,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
const teacherClass03: Teacher[] = teachers.slice(2, 3); const teacherClass03: Teacher[] = teachers.slice(2, 3);
const class03 = em.create(Class, { const class03 = em.create(Class, {
classId: 'id03', classId: '80dcc3e0-1811-4091-9361-42c0eee91cfa',
displayName: 'class03', displayName: 'class03',
teachers: teacherClass03, teachers: teacherClass03,
students: studentsClass03, students: studentsClass03,
@ -38,7 +38,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
const teacherClass04: Teacher[] = teachers.slice(2, 3); const teacherClass04: Teacher[] = teachers.slice(2, 3);
const class04 = em.create(Class, { const class04 = em.create(Class, {
classId: 'id04', classId: '33d03536-83b8-4880-9982-9bbf2f908ddf',
displayName: 'class04', displayName: 'class04',
teachers: teacherClass04, teachers: teacherClass04,
students: studentsClass04, students: studentsClass04,

View file

@ -2,12 +2,14 @@ import { EntityManager } from '@mikro-orm/core';
import { Question } from '../../../src/entities/questions/question.entity'; import { Question } from '../../../src/entities/questions/question.entity';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { Student } from '../../../src/entities/users/student.entity'; import { Student } from '../../../src/entities/users/student.entity';
import { Group } from '../../../src/entities/assignments/group.entity';
export function makeTestQuestions(em: EntityManager, students: Student[]): Question[] { export function makeTestQuestions(em: EntityManager, students: Student[], groups: Group[]): Question[] {
const question01 = em.create(Question, { const question01 = em.create(Question, {
learningObjectLanguage: Language.English, learningObjectLanguage: Language.English,
learningObjectVersion: 1, learningObjectVersion: 1,
learningObjectHruid: 'id05', learningObjectHruid: 'id05',
inGroup: groups[0], // Group #1 for Assignment #1 in class 'id01'
sequenceNumber: 1, sequenceNumber: 1,
author: students[0], author: students[0],
timestamp: new Date(), timestamp: new Date(),
@ -18,6 +20,7 @@ export function makeTestQuestions(em: EntityManager, students: Student[]): Quest
learningObjectLanguage: Language.English, learningObjectLanguage: Language.English,
learningObjectVersion: 1, learningObjectVersion: 1,
learningObjectHruid: 'id05', learningObjectHruid: 'id05',
inGroup: groups[0], // Group #1 for Assignment #1 in class 'id01'
sequenceNumber: 2, sequenceNumber: 2,
author: students[2], author: students[2],
timestamp: new Date(), timestamp: new Date(),
@ -30,6 +33,7 @@ export function makeTestQuestions(em: EntityManager, students: Student[]): Quest
learningObjectHruid: 'id04', learningObjectHruid: 'id04',
sequenceNumber: 1, sequenceNumber: 1,
author: students[0], author: students[0],
inGroup: groups[0], // Group #1 for Assignment #1 in class 'id01'
timestamp: new Date(), timestamp: new Date(),
content: 'question', content: 'question',
}); });
@ -40,9 +44,32 @@ export function makeTestQuestions(em: EntityManager, students: Student[]): Quest
learningObjectHruid: 'id01', learningObjectHruid: 'id01',
sequenceNumber: 1, sequenceNumber: 1,
author: students[1], author: students[1],
inGroup: groups[1], // Group #2 for Assignment #1 in class 'id01'
timestamp: new Date(), timestamp: new Date(),
content: 'question', content: 'question',
}); });
return [question01, question02, question03, question04]; const question05 = em.create(Question, {
learningObjectLanguage: Language.English,
learningObjectVersion: 1,
learningObjectHruid: 'id05',
sequenceNumber: 3,
author: students[1],
inGroup: groups[1], // Group #2 for Assignment #1 in class 'id01'
timestamp: new Date(),
content: 'question',
});
const question06 = em.create(Question, {
learningObjectLanguage: Language.English,
learningObjectVersion: 1,
learningObjectHruid: 'id05',
sequenceNumber: 4,
author: students[2],
inGroup: groups[3], // Group #4 for Assignment #2 in class 'id02'
timestamp: new Date(),
content: 'question',
});
return [question01, question02, question03, question04, question05, question06];
} }

View file

@ -11,6 +11,8 @@ export const TEST_STUDENTS = [
{ username: 'TheDoors', firstName: 'Jim', lastName: 'Morisson' }, { username: 'TheDoors', firstName: 'Jim', lastName: 'Morisson' },
// ⚠️ Deze mag niet gebruikt worden in elke test! // ⚠️ Deze mag niet gebruikt worden in elke test!
{ username: 'Nirvana', firstName: 'Kurt', lastName: 'Cobain' }, { username: 'Nirvana', firstName: 'Kurt', lastName: 'Cobain' },
// Makes sure when logged in as leerling1, there exists a corresponding user
{ username: 'testleerling1', firstName: 'Gerald', lastName: 'Schmittinger' },
]; ];
// 🏗️ Functie die ORM entities maakt uit de data array // 🏗️ Functie die ORM entities maakt uit de data array

View file

@ -27,5 +27,12 @@ export function makeTestTeachers(em: EntityManager): Teacher[] {
lastName: 'Cappelle', lastName: 'Cappelle',
}); });
return [teacher01, teacher02, teacher03, teacher04]; // Makes sure when logged in as testleerkracht1, there exists a corresponding user
const teacher05 = em.create(Teacher, {
username: 'testleerkracht1',
firstName: 'Bob',
lastName: 'Dylan',
});
return [teacher01, teacher02, teacher03, teacher04, teacher05];
} }

74
backend/tool/seed.ts Normal file
View file

@ -0,0 +1,74 @@
import { forkEntityManager, initORM } from '../src/orm.js';
import dotenv from 'dotenv';
import { makeTestAssignemnts } from '../tests/test_assets/assignments/assignments.testdata.js';
import { makeTestGroups } from '../tests/test_assets/assignments/groups.testdata.js';
import { makeTestSubmissions } from '../tests/test_assets/assignments/submission.testdata.js';
import { makeTestClassJoinRequests } from '../tests/test_assets/classes/class-join-requests.testdata.js';
import { makeTestClasses } from '../tests/test_assets/classes/classes.testdata.js';
import { makeTestTeacherInvitations } from '../tests/test_assets/classes/teacher-invitations.testdata.js';
import { makeTestAttachments } from '../tests/test_assets/content/attachments.testdata.js';
import { makeTestLearningObjects } from '../tests/test_assets/content/learning-objects.testdata.js';
import { makeTestLearningPaths } from '../tests/test_assets/content/learning-paths.testdata.js';
import { makeTestAnswers } from '../tests/test_assets/questions/answers.testdata.js';
import { makeTestQuestions } from '../tests/test_assets/questions/questions.testdata.js';
import { makeTestStudents } from '../tests/test_assets/users/students.testdata.js';
import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js';
import { getLogger, Logger } from '../src/logging/initalize.js';
import { Collection } from '@mikro-orm/core';
import { Group } from '../dist/entities/assignments/group.entity.js';
const logger: Logger = getLogger();
export async function seedDatabase(): Promise<void> {
dotenv.config({ path: '.env.development.local' });
const orm = await initORM();
await orm.schema.clearDatabase();
const em = forkEntityManager();
logger.info('seeding database...');
const students = makeTestStudents(em);
const teachers = makeTestTeachers(em);
const learningObjects = makeTestLearningObjects(em);
const learningPaths = makeTestLearningPaths(em);
const classes = makeTestClasses(em, students, teachers);
const assignments = makeTestAssignemnts(em, classes);
const groups = makeTestGroups(em, students, assignments);
assignments[0].groups = new Collection<Group>(groups.slice(0, 3));
assignments[1].groups = new Collection<Group>(groups.slice(3, 4));
const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes);
const classJoinRequests = makeTestClassJoinRequests(em, students, classes);
const attachments = makeTestAttachments(em, learningObjects);
learningObjects[1].attachments = attachments;
const questions = makeTestQuestions(em, students, groups);
const answers = makeTestAnswers(em, teachers, questions);
const submissions = makeTestSubmissions(em, students, groups);
// Persist all entities
await em.persistAndFlush([
...students,
...teachers,
...learningObjects,
...learningPaths,
...classes,
...assignments,
...groups,
...teacherInvitations,
...classJoinRequests,
...attachments,
...questions,
...answers,
...submissions,
]);
logger.info('Development database seeded successfully!');
await orm.close();
}
seedDatabase().catch(logger.error);

View file

@ -1,14 +1,19 @@
import { UserDTO } from './user';
import { QuestionDTO, QuestionId } from './question'; import { QuestionDTO, QuestionId } from './question';
import { TeacherDTO } from './teacher';
export interface AnswerDTO { export interface AnswerDTO {
author: UserDTO; author: TeacherDTO;
toQuestion: QuestionDTO; toQuestion: QuestionDTO;
sequenceNumber: number; sequenceNumber: number;
timestamp: string; timestamp: string;
content: string; content: string;
} }
export interface AnswerData {
author: string;
content: string;
}
export interface AnswerId { export interface AnswerId {
author: string; author: string;
toQuestion: QuestionId; toQuestion: QuestionId;

View file

@ -2,7 +2,7 @@ import { GroupDTO } from './group';
export interface AssignmentDTO { export interface AssignmentDTO {
id: number; id: number;
class: string; // Id of class 'within' within: string;
title: string; title: string;
description: string; description: string;
learningPath: string; learningPath: string;

View file

@ -3,5 +3,4 @@ export interface ClassDTO {
displayName: string; displayName: string;
teachers: string[]; teachers: string[];
students: string[]; students: string[];
joinRequests: string[];
} }

View file

@ -1,8 +1,10 @@
import { AssignmentDTO } from './assignment'; import { AssignmentDTO } from './assignment';
import { ClassDTO } from './class';
import { StudentDTO } from './student'; import { StudentDTO } from './student';
export interface GroupDTO { export interface GroupDTO {
class: string | ClassDTO;
assignment: number | AssignmentDTO; assignment: number | AssignmentDTO;
groupNumber: number; groupNumber: number;
members: string[] | StudentDTO[]; members?: string[] | StudentDTO[];
} }

View file

@ -11,7 +11,7 @@ export interface Transition {
}; };
} }
export interface LearningObjectIdentifier { export interface LearningObjectIdentifierDTO {
hruid: string; hruid: string;
language: Language; language: Language;
version?: number; version?: number;

View file

@ -1,15 +1,23 @@
import { LearningObjectIdentifier } from './learning-content'; import { LearningObjectIdentifierDTO } from './learning-content';
import { StudentDTO } from './student'; import { StudentDTO } from './student';
import { GroupDTO } from './group';
export interface QuestionDTO { export interface QuestionDTO {
learningObjectIdentifier: LearningObjectIdentifier; learningObjectIdentifier: LearningObjectIdentifierDTO;
sequenceNumber?: number; sequenceNumber?: number;
author: StudentDTO; author: StudentDTO;
timestamp?: string; inGroup: GroupDTO;
timestamp: string;
content: string; content: string;
} }
export interface QuestionData {
author?: string;
content: string;
inGroup: GroupDTO;
}
export interface QuestionId { export interface QuestionId {
learningObjectIdentifier: LearningObjectIdentifier; learningObjectIdentifier: LearningObjectIdentifierDTO;
sequenceNumber: number; sequenceNumber: number;
} }

View file

@ -1,15 +1,15 @@
import { GroupDTO } from './group'; import { GroupDTO } from './group';
import { LearningObjectIdentifier } from './learning-content'; import { LearningObjectIdentifierDTO } from './learning-content';
import { StudentDTO } from './student'; import { StudentDTO } from './student';
import { Language } from '../util/language'; import { Language } from '../util/language';
export interface SubmissionDTO { export interface SubmissionDTO {
learningObjectIdentifier: LearningObjectIdentifier; learningObjectIdentifier: LearningObjectIdentifierDTO;
submissionNumber?: number; submissionNumber?: number;
submitter: StudentDTO; submitter: StudentDTO;
time?: Date; time?: Date;
group?: GroupDTO; group: GroupDTO;
content: string; content: string;
} }

View file

@ -24,7 +24,7 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
# TODO Replace with environment keys # TODO Replace with environment keys
- ./backend/.env:/app/.env - ./backend/.env:/app/dwengo/backend/.env
depends_on: depends_on:
- db - db
- logging - logging

View file

@ -24,7 +24,7 @@ services:
- '3000:3000/tcp' - '3000:3000/tcp'
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./backend/.env.staging:/app/.env - ./backend/.env.staging:/app/dwengo/backend/.env
depends_on: depends_on:
- db - db
- logging - logging

1
docs/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
api/swagger.json

View file

@ -15,6 +15,10 @@ const doc = {
url: 'http://localhost:3000/', url: 'http://localhost:3000/',
description: 'Development server', description: 'Development server',
}, },
{
url: 'http://localhost/',
description: 'Staging server',
},
{ {
url: 'https://sel2-1.ugent.be/', url: 'https://sel2-1.ugent.be/',
description: 'Production server', description: 'Production server',
@ -55,4 +59,4 @@ const doc = {
const outputFile = './swagger.json'; const outputFile = './swagger.json';
const routes = ['../../backend/src/app.ts']; const routes = ['../../backend/src/app.ts'];
await swaggerAutogen({ openapi: '3.1.0' })(outputFile, routes, doc); void swaggerAutogen({ openapi: '3.1.0' })(outputFile, routes, doc);

File diff suppressed because it is too large Load diff

View file

@ -44,16 +44,16 @@ export default [
// All @typescript-eslint configuration options are listed. // All @typescript-eslint configuration options are listed.
// If the rules are commented, they are configured by the inherited configurations. // If the rules are commented, they are configured by the inherited configurations.
'@typescript-eslint/adjacent-overload-signatures': 'warn', '@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/array-type': 'warn', '@typescript-eslint/array-type': 'error',
'@typescript-eslint/await-thenable': 'error', '@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/ban-ts-comment': ['error', { minimumDescriptionLength: 10 }], '@typescript-eslint/ban-ts-comment': ['error', { minimumDescriptionLength: 10 }],
'@typescript-eslint/ban-tslint-comment': 'error', '@typescript-eslint/ban-tslint-comment': 'error',
camelcase: 'off', camelcase: 'off',
'@typescript-eslint/class-literal-property-style': 'warn', '@typescript-eslint/class-literal-property-style': 'error',
'class-methods-use-this': 'off', 'class-methods-use-this': 'off',
'@typescript-eslint/class-methods-use-this': ['error', { ignoreOverrideMethods: true }], '@typescript-eslint/class-methods-use-this': ['error', { ignoreOverrideMethods: true }],
'@typescript-eslint/consistent-generic-constructors': 'warn', '@typescript-eslint/consistent-generic-constructors': 'error',
'@typescript-eslint/consistent-indexed-object-style': 'error', '@typescript-eslint/consistent-indexed-object-style': 'error',
'consistent-return': 'off', 'consistent-return': 'off',
'@typescript-eslint/consistent-return': 'off', '@typescript-eslint/consistent-return': 'off',
@ -64,18 +64,18 @@ export default [
'default-param-last': 'off', 'default-param-last': 'off',
'@typescript-eslint/default-param-last': 'error', '@typescript-eslint/default-param-last': 'error',
'dot-notation': 'off', 'dot-notation': 'off',
'@typescript-eslint/dot-notation': 'warn', '@typescript-eslint/dot-notation': 'error',
'@typescript-eslint/explicit-function-return-type': 'warn', '@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/explicit-member-accessibility': 'off', '@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'warn', '@typescript-eslint/explicit-module-boundary-types': 'error',
'init-declarations': 'off', 'init-declarations': 'off',
'@typescript-eslint/init-declarations': 'off', '@typescript-eslint/init-declarations': 'off',
'max-params': 'off', 'max-params': 'off',
'@typescript-eslint/max-params': ['error', { max: 6 }], '@typescript-eslint/max-params': ['error', { max: 6 }],
'@typescript-eslint/member-ordering': 'warn', '@typescript-eslint/member-ordering': 'error',
'@typescript-eslint/method-signature-style': 'off', // Don't care about TypeScript strict mode. '@typescript-eslint/method-signature-style': 'off', // Don't care about TypeScript strict mode.
'@typescript-eslint/naming-convention': [ '@typescript-eslint/naming-convention': [
'warn', 'error',
{ {
// Enforce that all variables, functions and properties are camelCase // Enforce that all variables, functions and properties are camelCase
selector: 'variableLike', selector: 'variableLike',
@ -113,7 +113,7 @@ export default [
'@typescript-eslint/no-empty-function': 'error', '@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-empty-object-type': 'error',
'@typescript-eslint/no-explicit-any': 'warn', // Once in production, this should be an error. '@typescript-eslint/no-explicit-any': 'error', // Once in production, this should be an error.
'@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error',
'@typescript-eslint/no-extraneous-class': 'error', '@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-floating-promises': 'error',
@ -121,7 +121,7 @@ export default [
'no-implied-eval': 'off', 'no-implied-eval': 'off',
'@typescript-eslint/no-implied-eval': 'error', '@typescript-eslint/no-implied-eval': 'error',
'@typescript-eslint/no-import-type-side-effects': 'error', '@typescript-eslint/no-import-type-side-effects': 'error',
'@typescript-eslint/no-inferrable-types': 'warn', '@typescript-eslint/no-inferrable-types': 'error',
'no-invalid-this': 'off', 'no-invalid-this': 'off',
'@typescript-eslint/no-invalid-this': 'off', '@typescript-eslint/no-invalid-this': 'off',
'@typescript-eslint/no-invalid-void-type': 'error', '@typescript-eslint/no-invalid-void-type': 'error',
@ -146,10 +146,10 @@ export default [
'@typescript-eslint/no-unsafe-function-type': 'error', '@typescript-eslint/no-unsafe-function-type': 'error',
'no-unused-expressions': 'off', 'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': 'warn', '@typescript-eslint/no-unused-expressions': 'error',
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
'warn', 'error',
{ {
args: 'all', args: 'all',
argsIgnorePattern: '^_', argsIgnorePattern: '^_',
@ -164,53 +164,53 @@ export default [
'@typescript-eslint/parameter-properties': 'off', '@typescript-eslint/parameter-properties': 'off',
'@typescript-eslint/prefer-find': 'warn', '@typescript-eslint/prefer-find': 'error',
'@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/prefer-function-type': 'error',
'@typescript-eslint/prefer-readonly-parameter-types': 'off', '@typescript-eslint/prefer-readonly-parameter-types': 'off',
'@typescript-eslint/prefer-reduce-type-parameter': 'error', '@typescript-eslint/prefer-reduce-type-parameter': 'error',
'@typescript-eslint/promise-function-async': 'warn', '@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/require-array-sort-compare': 'warn', '@typescript-eslint/require-array-sort-compare': 'error',
'no-await-in-loop': 'warn', 'no-await-in-loop': 'error',
'no-constructor-return': 'error', 'no-constructor-return': 'error',
'no-inner-declarations': 'error', 'no-inner-declarations': 'error',
'no-self-compare': 'error', 'no-self-compare': 'error',
'no-template-curly-in-string': 'error', 'no-template-curly-in-string': 'error',
'no-unmodified-loop-condition': 'warn', 'no-unmodified-loop-condition': 'error',
'no-unreachable-loop': 'warn', 'no-unreachable-loop': 'error',
'no-useless-assignment': 'error', 'no-useless-assignment': 'error',
'arrow-body-style': ['warn', 'as-needed'], 'arrow-body-style': ['error', 'as-needed'],
'block-scoped-var': 'warn', 'block-scoped-var': 'error',
'capitalized-comments': 'warn', 'capitalized-comments': 'error',
'consistent-this': 'error', 'consistent-this': 'error',
curly: 'error', curly: 'error',
'default-case': 'error', 'default-case': 'error',
'default-case-last': 'error', 'default-case-last': 'error',
eqeqeq: 'error', eqeqeq: 'error',
'func-names': 'warn', 'func-names': 'error',
'func-style': ['warn', 'declaration'], 'func-style': ['error', 'declaration'],
'grouped-accessor-pairs': ['warn', 'getBeforeSet'], 'grouped-accessor-pairs': ['error', 'getBeforeSet'],
'guard-for-in': 'warn', 'guard-for-in': 'error',
'logical-assignment-operators': 'warn', 'logical-assignment-operators': 'error',
'max-classes-per-file': 'warn', 'max-classes-per-file': 'error',
'no-alert': 'error', 'no-alert': 'error',
'no-bitwise': 'warn', 'no-bitwise': 'error',
'no-console': 'warn', 'no-console': 'error',
'no-continue': 'warn', 'no-continue': 'error',
'no-else-return': 'warn', 'no-else-return': 'error',
'no-eq-null': 'error', 'no-eq-null': 'error',
'no-eval': 'error', 'no-eval': 'error',
'no-extend-native': 'error', 'no-extend-native': 'error',
'no-extra-label': 'error', 'no-extra-label': 'error',
'no-implicit-coercion': 'warn', 'no-implicit-coercion': 'error',
'no-iterator': 'error', 'no-iterator': 'error',
'no-label-var': 'warn', 'no-label-var': 'error',
'no-labels': 'warn', 'no-labels': 'error',
'no-multi-assign': 'error', 'no-multi-assign': 'error',
'no-nested-ternary': 'error', 'no-nested-ternary': 'error',
'no-object-constructor': 'error', 'no-object-constructor': 'error',

View file

@ -21,6 +21,7 @@
"@tanstack/vue-query": "^5.69.0", "@tanstack/vue-query": "^5.69.0",
"axios": "^1.8.2", "axios": "^1.8.2",
"oidc-client-ts": "^3.1.0", "oidc-client-ts": "^3.1.0",
"uuid": "^11.1.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.2", "vue-i18n": "^11.1.2",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",

View file

@ -0,0 +1,39 @@
import type { AnswerData, AnswerDTO, AnswerId } from "@dwengo-1/common/interfaces/answer";
import { BaseController } from "@/controllers/base-controller.ts";
import type { QuestionId } from "@dwengo-1/common/interfaces/question";
export interface AnswersResponse {
answers: AnswerDTO[] | AnswerId[];
}
export interface AnswerResponse {
answer: AnswerDTO;
}
export class AnswerController extends BaseController {
constructor(questionId: QuestionId) {
this.loId = questionId.learningObjectIdentifier;
this.sequenceNumber = questionId.sequenceNumber;
super(`learningObject/${loId.hruid}/:${loId.version}/questions/${this.sequenceNumber}/answers`);
}
async getAll(full = true): Promise<AnswersResponse> {
return this.get<AnswersResponse>("/", { lang: this.loId.lang, full });
}
async getBy(seq: number): Promise<AnswerResponse> {
return this.get<AnswerResponse>(`/${seq}`, { lang: this.loId.lang });
}
async create(answerData: AnswerData): Promise<AnswerResponse> {
return this.post<AnswerResponse>("/", answerData, { lang: this.loId.lang });
}
async remove(seq: number): Promise<AnswerResponse> {
return this.delete<AnswerResponse>(`/${seq}`, { lang: this.loId.lang });
}
async update(seq: number, answerData: AnswerData): Promise<AnswerResponse> {
return this.put<AnswerResponse>(`/${seq}`, answerData, { lang: this.loId.lang });
}
}

View file

@ -1,5 +1,51 @@
import type { AssignmentDTO } from "@dwengo-1/interfaces/assignment"; import { BaseController } from "./base-controller";
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
import type { SubmissionsResponse } from "./submissions";
import type { QuestionsResponse } from "./questions";
import type { GroupsResponse } from "./groups";
export interface AssignmentsResponse { export interface AssignmentsResponse {
assignments: AssignmentDTO[]; assignments: AssignmentDTO[] | string[];
} // TODO ID }
export interface AssignmentResponse {
assignment: AssignmentDTO;
}
export class AssignmentController extends BaseController {
constructor(classid: string) {
super(`class/${classid}/assignments`);
}
async getAll(full = true): Promise<AssignmentsResponse> {
return this.get<AssignmentsResponse>(`/`, { full });
}
async getByNumber(num: number): Promise<AssignmentResponse> {
return this.get<AssignmentResponse>(`/${num}`);
}
async createAssignment(data: AssignmentDTO): Promise<AssignmentResponse> {
return this.post<AssignmentResponse>(`/`, data);
}
async deleteAssignment(num: number): Promise<AssignmentResponse> {
return this.delete<AssignmentResponse>(`/${num}`);
}
async updateAssignment(num: number, data: Partial<AssignmentDTO>): Promise<AssignmentResponse> {
return this.put<AssignmentResponse>(`/${num}`, data);
}
async getSubmissions(assignmentNumber: number, full = true): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(`/${assignmentNumber}/submissions`, { full });
}
async getQuestions(assignmentNumber: number, full = true): Promise<QuestionsResponse> {
return this.get<QuestionsResponse>(`/${assignmentNumber}/questions`, { full });
}
async getGroups(assignmentNumber: number, full = true): Promise<GroupsResponse> {
return this.get<GroupsResponse>(`/${assignmentNumber}/groups`, { full });
}
}

View file

@ -10,7 +10,7 @@ export abstract class BaseController {
} }
private static assertSuccessResponse(response: AxiosResponse<unknown, unknown>): void { private static assertSuccessResponse(response: AxiosResponse<unknown, unknown>): void {
if (response.status / 100 !== 2) { if (response.status < 200 || response.status >= 300) {
throw new HttpErrorResponseException(response); throw new HttpErrorResponseException(response);
} }
} }
@ -21,20 +21,20 @@ export abstract class BaseController {
return response.data; return response.data;
} }
protected async post<T>(path: string, body: unknown): Promise<T> { protected async post<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> {
const response = await apiClient.post<T>(this.absolutePathFor(path), body); const response = await apiClient.post<T>(this.absolutePathFor(path), body, { params: queryParams });
BaseController.assertSuccessResponse(response); BaseController.assertSuccessResponse(response);
return response.data; return response.data;
} }
protected async delete<T>(path: string): Promise<T> { protected async delete<T>(path: string, queryParams?: QueryParams): Promise<T> {
const response = await apiClient.delete<T>(this.absolutePathFor(path)); const response = await apiClient.delete<T>(this.absolutePathFor(path), { params: queryParams });
BaseController.assertSuccessResponse(response); BaseController.assertSuccessResponse(response);
return response.data; return response.data;
} }
protected async put<T>(path: string, body: unknown): Promise<T> { protected async put<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> {
const response = await apiClient.put<T>(this.absolutePathFor(path), body); const response = await apiClient.put<T>(this.absolutePathFor(path), body, { params: queryParams });
BaseController.assertSuccessResponse(response); BaseController.assertSuccessResponse(response);
return response.data; return response.data;
} }

View file

@ -1,5 +1,80 @@
import type { ClassDTO } from "@dwengo-1/interfaces/class"; import { BaseController } from "./base-controller";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import type { StudentsResponse } from "./students";
import type { AssignmentsResponse } from "./assignments";
import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
import type { TeachersResponse } from "@/controllers/teachers.ts";
export interface ClassesResponse { export interface ClassesResponse {
classes: ClassDTO[] | string[]; classes: ClassDTO[] | string[];
} }
export interface ClassResponse {
class: ClassDTO;
}
export interface TeacherInvitationsResponse {
invites: TeacherInvitationDTO[];
}
export interface TeacherInvitationResponse {
invite: TeacherInvitationDTO;
}
export class ClassController extends BaseController {
constructor() {
super("class");
}
async getAll(full = true): Promise<ClassesResponse> {
return this.get<ClassesResponse>(`/`, { full });
}
async getById(id: string): Promise<ClassResponse> {
return this.get<ClassResponse>(`/${id}`);
}
async createClass(data: ClassDTO): Promise<ClassResponse> {
return this.post<ClassResponse>(`/`, data);
}
async deleteClass(id: string): Promise<ClassResponse> {
return this.delete<ClassResponse>(`/${id}`);
}
async updateClass(id: string, data: Partial<ClassDTO>): Promise<ClassResponse> {
return this.put<ClassResponse>(`/${id}`, data);
}
async getStudents(id: string, full = true): Promise<StudentsResponse> {
return this.get<StudentsResponse>(`/${id}/students`, { full });
}
async addStudent(id: string, username: string): Promise<ClassResponse> {
return this.post<ClassResponse>(`/${id}/students`, { username });
}
async deleteStudent(id: string, username: string): Promise<ClassResponse> {
return this.delete<ClassResponse>(`/${id}/students/${username}`);
}
async getTeachers(id: string, full = true): Promise<TeachersResponse> {
return this.get<TeachersResponse>(`/${id}/teachers`, { full });
}
async addTeacher(id: string, username: string): Promise<ClassResponse> {
return this.post<ClassResponse>(`/${id}/teachers`, { username });
}
async deleteTeacher(id: string, username: string): Promise<ClassResponse> {
return this.delete<ClassResponse>(`/${id}/teachers/${username}`);
}
async getTeacherInvitations(id: string, full = true): Promise<TeacherInvitationsResponse> {
return this.get<TeacherInvitationsResponse>(`/${id}/teacher-invitations`, { full });
}
async getAssignments(id: string, full = true): Promise<AssignmentsResponse> {
return this.get<AssignmentsResponse>(`/${id}/assignments`, { full });
}
}

View file

@ -1,6 +1,7 @@
import { ThemeController } from "@/controllers/themes.ts"; import { ThemeController } from "@/controllers/themes.ts";
import { LearningObjectController } from "@/controllers/learning-objects.ts"; import { LearningObjectController } from "@/controllers/learning-objects.ts";
import { LearningPathController } from "@/controllers/learning-paths.ts"; import { LearningPathController } from "@/controllers/learning-paths.ts";
import { ClassController } from "@/controllers/classes.ts";
export function controllerGetter<T>(factory: new () => T): () => T { export function controllerGetter<T>(factory: new () => T): () => T {
let instance: T | undefined; let instance: T | undefined;
@ -16,3 +17,4 @@ export function controllerGetter<T>(factory: new () => T): () => T {
export const getThemeController = controllerGetter(ThemeController); export const getThemeController = controllerGetter(ThemeController);
export const getLearningObjectController = controllerGetter(LearningObjectController); export const getLearningObjectController = controllerGetter(LearningObjectController);
export const getLearningPathController = controllerGetter(LearningPathController); export const getLearningPathController = controllerGetter(LearningPathController);
export const getClassController = controllerGetter(ClassController);

View file

@ -1,5 +1,46 @@
import type { GroupDTO } from "@dwengo-1/interfaces/group"; import { BaseController } from "./base-controller";
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
import type { SubmissionsResponse } from "./submissions";
import type { QuestionsResponse } from "./questions";
export interface GroupsResponse { export interface GroupsResponse {
groups: GroupDTO[]; groups: GroupDTO[];
} // | TODO id }
export interface GroupResponse {
group: GroupDTO;
}
export class GroupController extends BaseController {
constructor(classid: string, assignmentNumber: number) {
super(`class/${classid}/assignments/${assignmentNumber}/groups`);
}
async getAll(full = true): Promise<GroupsResponse> {
return this.get<GroupsResponse>(`/`, { full });
}
async getByNumber(num: number): Promise<GroupResponse> {
return this.get<GroupResponse>(`/${num}`);
}
async createGroup(data: GroupDTO): Promise<GroupResponse> {
return this.post<GroupResponse>(`/`, data);
}
async deleteGroup(num: number): Promise<GroupResponse> {
return this.delete<GroupResponse>(`/${num}`);
}
async updateGroup(num: number, data: Partial<GroupDTO>): Promise<GroupResponse> {
return this.put<GroupResponse>(`/${num}`, data);
}
async getSubmissions(groupNumber: number, full = true): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(`/${groupNumber}/submissions`, { full });
}
async getQuestions(groupNumber: number, full = true): Promise<QuestionsResponse> {
return this.get<QuestionsResponse>(`/${groupNumber}/questions`, { full });
}
}

View file

@ -1,5 +1,38 @@
import type { QuestionDTO, QuestionId } from "@dwengo-1/interfaces/question"; import type { QuestionData, QuestionDTO, QuestionId } from "@dwengo-1/common/interfaces/question";
import { BaseController } from "@/controllers/base-controller.ts";
import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content";
export interface QuestionsResponse { export interface QuestionsResponse {
questions: QuestionDTO[] | QuestionId[]; questions: QuestionDTO[] | QuestionId[];
} }
export interface QuestionResponse {
question: QuestionDTO;
}
export class QuestionController extends BaseController {
constructor(loId: LearningObjectIdentifierDTO) {
this.loId = loId;
super(`learningObject/${loId.hruid}/:${loId.version}/questions`);
}
async getAll(full = true): Promise<QuestionsResponse> {
return this.get<QuestionsResponse>("/", { lang: this.loId.lang, full });
}
async getBy(sequenceNumber: number): Promise<QuestionResponse> {
return this.get<QuestionResponse>(`/${sequenceNumber}`, { lang: this.loId.lang });
}
async create(questionData: QuestionData): Promise<QuestionResponse> {
return this.post<QuestionResponse>("/", questionData, { lang: this.loId.lang });
}
async remove(sequenceNumber: number): Promise<QuestionResponse> {
return this.delete<QuestionResponse>(`/${sequenceNumber}`, { lang: this.loId.lang });
}
async update(sequenceNumber: number, questionData: QuestionData): Promise<QuestionResponse> {
return this.put<QuestionResponse>(`/${sequenceNumber}`, questionData, { lang: this.loId.lang });
}
}

View file

@ -4,8 +4,8 @@ import type { AssignmentsResponse } from "@/controllers/assignments.ts";
import type { GroupsResponse } from "@/controllers/groups.ts"; import type { GroupsResponse } from "@/controllers/groups.ts";
import type { SubmissionsResponse } from "@/controllers/submissions.ts"; import type { SubmissionsResponse } from "@/controllers/submissions.ts";
import type { QuestionsResponse } from "@/controllers/questions.ts"; import type { QuestionsResponse } from "@/controllers/questions.ts";
import type { StudentDTO } from "@dwengo-1/interfaces/student"; import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
import type { ClassJoinRequestDTO } from "@dwengo-1/interfaces/class-join-request"; import type { ClassJoinRequestDTO } from "@dwengo-1/common/interfaces/class-join-request";
export interface StudentsResponse { export interface StudentsResponse {
students: StudentDTO[] | string[]; students: StudentDTO[] | string[];
@ -70,7 +70,7 @@ export class StudentController extends BaseController {
} }
async createJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> { async createJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> {
return this.post<JoinRequestResponse>(`/${username}/joinRequests}`, classId); return this.post<JoinRequestResponse>(`/${username}/joinRequests`, { classId });
} }
async deleteJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> { async deleteJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> {

View file

@ -1,5 +1,32 @@
import { type SubmissionDTO, SubmissionDTOId } from "@dwengo-1/interfaces/submission"; import { BaseController } from "./base-controller";
import type { SubmissionDTO, SubmissionDTOId } from "@dwengo-1/common/interfaces/submission";
export interface SubmissionsResponse { export interface SubmissionsResponse {
submissions: SubmissionDTO[] | SubmissionDTOId[]; submissions: SubmissionDTO[] | SubmissionDTOId[];
} }
export interface SubmissionResponse {
submission: SubmissionDTO;
}
export class SubmissionController extends BaseController {
constructor(classid: string, assignmentNumber: number, groupNumber: number) {
super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}`);
}
async getAll(full = true): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(`/`, { full });
}
async getByNumber(submissionNumber: number): Promise<SubmissionResponse> {
return this.get<SubmissionResponse>(`/${submissionNumber}`);
}
async createSubmission(data: unknown): Promise<SubmissionResponse> {
return this.post<SubmissionResponse>(`/`, data);
}
async deleteSubmission(submissionNumber: number): Promise<SubmissionResponse> {
return this.delete<SubmissionResponse>(`/${submissionNumber}`);
}
}

View file

@ -2,7 +2,7 @@ import { BaseController } from "@/controllers/base-controller.ts";
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts"; import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
import type { QuestionsResponse } from "@/controllers/questions.ts"; import type { QuestionsResponse } from "@/controllers/questions.ts";
import type { ClassesResponse } from "@/controllers/classes.ts"; import type { ClassesResponse } from "@/controllers/classes.ts";
import type { TeacherDTO } from "@dwengo-1/interfaces/teacher"; import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
export interface TeachersResponse { export interface TeachersResponse {
teachers: TeacherDTO[] | string[]; teachers: TeacherDTO[] | string[];

View file

@ -1,5 +1,5 @@
import { BaseController } from "@/controllers/base-controller.ts"; import { BaseController } from "@/controllers/base-controller.ts";
import type { Theme } from "@dwengo-1/interfaces/theme"; import type { Theme } from "@dwengo-1/common/interfaces/theme";
export class ThemeController extends BaseController { export class ThemeController extends BaseController {
constructor() { constructor() {

View file

@ -17,6 +17,11 @@
"inclusive": "Inclusiv", "inclusive": "Inclusiv",
"sociallyRelevant": "Gesellschaftlich relevant", "sociallyRelevant": "Gesellschaftlich relevant",
"translate": "übersetzen", "translate": "übersetzen",
"joinClass": "Klasse beitreten",
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
"invalidFormat": "Ungültiges Format",
"submitCode": "senden",
"members": "Mitglieder",
"themes": "Themen", "themes": "Themen",
"choose-theme": "Wähle ein thema", "choose-theme": "Wähle ein thema",
"choose-age": "Alter auswählen", "choose-age": "Alter auswählen",
@ -51,5 +56,24 @@
"noLearningPathsFoundDescription": "Es gibt keine Lernpfade, die zu Ihrem Suchbegriff passen.", "noLearningPathsFoundDescription": "Es gibt keine Lernpfade, die zu Ihrem Suchbegriff passen.",
"legendNotCompletedYet": "Noch nicht fertig", "legendNotCompletedYet": "Noch nicht fertig",
"legendCompleted": "Fertig", "legendCompleted": "Fertig",
"legendTeacherExclusive": "Information für Lehrkräfte" "legendTeacherExclusive": "Information für Lehrkräfte",
"code": "code",
"class": "Klasse",
"invitations": "Einladungen",
"createClass": "Klasse erstellen",
"createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.",
"classname": "Klassenname",
"EnterNameOfClass": "einen Klassennamen eingeben.",
"create": "erstellen",
"sender": "Absender",
"nameIsMandatory": "Der Klassenname ist ein Pflichtfeld",
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
"close": "schließen",
"copied": "kopiert!",
"accept": "akzeptieren",
"deny": "ablehnen",
"sent": "sent",
"failed": "gescheitert",
"wrong": "etwas ist schief gelaufen",
"created": "erstellt"
} }

View file

@ -29,6 +29,11 @@
"sociallyRelevant": "Socially relevant", "sociallyRelevant": "Socially relevant",
"login": "log in", "login": "log in",
"translate": "translate", "translate": "translate",
"joinClass": "Join class",
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
"invalidFormat": "Invalid format.",
"submitCode": "submit",
"members": "members",
"themes": "Themes", "themes": "Themes",
"choose-theme": "Select a theme", "choose-theme": "Select a theme",
"choose-age": "Select age", "choose-age": "Select age",
@ -51,5 +56,24 @@
"high-school": "16-18 years old", "high-school": "16-18 years old",
"older": "18 and older" "older": "18 and older"
}, },
"read-more": "Read more" "read-more": "Read more",
"code": "code",
"class": "class",
"invitations": "invitations",
"createClass": "create class",
"classname": "classname",
"EnterNameOfClass": "Enter a classname.",
"create": "create",
"sender": "sender",
"nameIsMandatory": "classname is mandatory",
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
"close": "close",
"copied": "copied!",
"accept": "accept",
"deny": "deny",
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.",
"sent": "sent",
"failed": "failed",
"wrong": "something went wrong",
"created": "created"
} }

View file

@ -29,6 +29,11 @@
"inclusive": "Inclusif", "inclusive": "Inclusif",
"sociallyRelevant": "Socialement pertinent", "sociallyRelevant": "Socialement pertinent",
"translate": "traduire", "translate": "traduire",
"joinClass": "Rejoindre une classe",
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
"invalidFormat": "Format non valide.",
"submitCode": "envoyer",
"members": "membres",
"themes": "Thèmes", "themes": "Thèmes",
"choose-theme": "Choisis un thème", "choose-theme": "Choisis un thème",
"choose-age": "Choisis un âge", "choose-age": "Choisis un âge",
@ -51,5 +56,24 @@
"high-school": "16-18 ans", "high-school": "16-18 ans",
"older": "18 et plus" "older": "18 et plus"
}, },
"read-more": "En savoir plus" "read-more": "En savoir plus",
"code": "code",
"class": "classe",
"invitations": "invitations",
"createClass": "créer une classe",
"createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.",
"classname": "nom de classe",
"EnterNameOfClass": "saisir un nom de classe.",
"create": "créer",
"sender": "expéditeur",
"nameIsMandatory": "le nom de classe est obligatoire",
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
"close": "fermer",
"copied": "copié!",
"accept": "accepter",
"deny": "refuser",
"sent": "envoyé",
"failed": "échoué",
"wrong": "quelque chose n'a pas fonctionné",
"created": "créé"
} }

Some files were not shown because too many files have changed in this diff Show more