# Conflicts:
#	backend/src/controllers/questions.ts
#	backend/src/controllers/submissions.ts
#	backend/src/data/questions/question-repository.ts
#	backend/src/interfaces/group.ts
#	backend/src/interfaces/question.ts
#	backend/src/interfaces/submission.ts
#	backend/src/routes/submissions.ts
#	backend/src/services/groups.ts
#	backend/src/services/questions.ts
#	backend/src/services/students.ts
#	backend/src/services/submissions.ts
#	common/src/interfaces/question.ts
This commit is contained in:
Gerald Schmittinger 2025-04-09 20:25:30 +02:00
commit d6dd7fb3bf
90 changed files with 2934 additions and 792 deletions

View file

@ -44,7 +44,6 @@ RUN npm install --silent --only=production
COPY ./docs ./docs 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

@ -3,175 +3,122 @@ import {
createQuestion, createQuestion,
deleteQuestion, deleteQuestion,
getAllQuestions, getAllQuestions,
getAnswersByQuestion,
getQuestion, getQuestion,
getQuestionsAboutLearningObjectInAssignment, getQuestionsAboutLearningObjectInAssignment,
updateQuestion
} from '../services/questions.js'; } from '../services/questions.js';
import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.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 { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; import { requireFields } from './error-helper.js';
interface QuestionPathParams {
hruid: string;
version: string;
}
interface QuestionQueryParams {
lang: string;
}
function getObjectId<ResBody, ReqBody>(
req: Request<QuestionPathParams, ResBody, ReqBody, QuestionQueryParams>,
res: Response
): LearningObjectIdentifier | null {
const { hruid, version } = req.params;
const lang = req.query.lang;
if (!hruid || !version) {
res.status(400).json({ error: 'Missing required parameters.' });
return null;
}
export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier {
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,
}; };
} }
interface GetQuestionIdPathParams extends QuestionPathParams { export function getQuestionId(learningObjectIdentifier: LearningObjectIdentifier, seq: string): QuestionId {
seq: string;
}
function getQuestionId<ReqBody, ResBody>(
req: Request<GetQuestionIdPathParams, ReqBody, ResBody, QuestionQueryParams>,
res: Response
): QuestionId | null {
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,
}; };
} }
interface GetAllQuestionsQueryParams extends QuestionQueryParams { export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> {
classId?: string; const hruid = req.params.hruid;
assignmentId?: number; const version = req.params.version;
forStudent?: string; const language = req.query.lang as string;
full?: boolean; const full = req.query.full === 'true';
} requireFields({ hruid });
export async function getAllQuestionsHandler( const learningObjectId = getLearningObjectId(hruid, version, language);
req: Request<QuestionPathParams, QuestionDTO[] | QuestionId[], unknown, GetAllQuestionsQueryParams>,
res: Response
): Promise<void> {
const objectId = getObjectId(req, res);
const full = req.query.full;
if (!objectId) {
return;
}
let questions: QuestionDTO[] | QuestionId[]; let questions: QuestionDTO[] | QuestionId[];
if (req.query.classId && req.query.assignmentId) { if (req.query.classId && req.query.assignmentId) {
questions = await getQuestionsAboutLearningObjectInAssignment( questions = await getQuestionsAboutLearningObjectInAssignment(
objectId, learningObjectId,
req.query.classId, req.query.classId as string,
req.query.assignmentId, parseInt(req.query.assignmentId as string),
full ?? false, full ?? false,
req.query.forStudent req.query.forStudent as string | undefined
); );
} else { } else {
questions = await getAllQuestions(objectId, full ?? false); questions = await getAllQuestions(learningObjectId, full ?? false);
} }
if (!questions) { res.json({ questions });
res.status(404).json({ error: `Questions not found.` });
} else {
res.json({ questions: questions });
}
} }
export async function getQuestionHandler( export async function getQuestionHandler(req: Request, res: Response): Promise<void> {
req: Request<GetQuestionIdPathParams, QuestionDTO[] | QuestionId[], unknown, QuestionQueryParams>, const hruid = req.params.hruid;
res: Response const version = req.params.version;
): Promise<void> { const language = req.query.lang as string;
const questionId = getQuestionId(req, res); 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);
}
}
interface GetQuestionAnswersQueryParams extends QuestionQueryParams {
full: boolean;
}
export async function getQuestionAnswersHandler(
req: Request<GetQuestionIdPathParams, { answers: AnswerDTO[] | AnswerId[] }, unknown, GetQuestionAnswersQueryParams>,
res: Response
): Promise<void> {
const questionId = getQuestionId(req, res);
const full = req.query.full;
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.inGroup || !questionDTO.content) { const loId = getLearningObjectId(hruid, version, language);
res.status(400).json({ error: 'Missing required fields: identifier, author, inGroup, 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( export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> {
req: Request<GetQuestionIdPathParams, QuestionDTO, unknown, QuestionQueryParams>, const hruid = req.params.hruid;
res: Response const version = req.params.version;
): Promise<void> { const language = req.query.lang as string;
const questionId = getQuestionId(req, res); 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,28 +1,20 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createSubmission, deleteSubmission, getSubmission, getSubmissionsForLearningObjectAndAssignment } 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 { Submission } from '../entities/assignments/submission.entity'; 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;
id: number;
}
interface SubmissionQuery {
language: string;
version: number;
}
interface SubmissionsQuery extends SubmissionQuery {
classId: string;
assignmentId: number;
studentUsername?: string;
}
export async function getSubmissionsHandler(req: Request<SubmissionParams, Submission[], null, SubmissionsQuery>, res: Response): Promise<void> {
const loHruid = req.params.hruid; const loHruid = req.params.hruid;
const lang = languageMap[req.query.language] || Language.Dutch; const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = req.query.version || 1; const version = req.query.version || 1;
const submissions = await getSubmissionsForLearningObjectAndAssignment(loHruid, lang, version, req.query.classId, req.query.assignmentId); const submissions = await getSubmissionsForLearningObjectAndAssignment(loHruid, lang, version, req.query.classId, req.query.assignmentId);
@ -30,54 +22,57 @@ export async function getSubmissionsHandler(req: Request<SubmissionParams, Submi
res.json(submissions); 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

@ -18,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(
{ {

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,8 +3,9 @@ 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'; import { Group } from '../../entities/assignments/group.entity';
import { Assignment } from '../../entities/assignments/assignment.entity';
export class QuestionRepository extends DwengoEntityRepository<Question> { export class QuestionRepository extends DwengoEntityRepository<Question> {
public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> { public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> {
@ -59,6 +60,14 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
}); });
} }
public async findAllByAssignment(assignment: Assignment): Promise<Question[]> {
return this.find({
author: assignment.groups.flatMap((group) => group.members),
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 },
@ -97,4 +106,19 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
}, },
}); });
} }
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,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,14 @@
import { Group } from '../entities/assignments/group.entity.js'; import { Group } from '../entities/assignments/group.entity.js';
import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from './assignment.js'; import { mapToAssignment } from './assignment.js';
import { mapToStudent, mapToStudentDTO } from './student.js'; import { mapToStudent } from './student.js';
import { mapToAssignmentDTO } from './assignment.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 { getGroupRepository } from '../data/repositories.js';
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
import { Class } from '../entities/classes/class.entity.js'; import { Class } from '../entities/classes/class.entity.js';
import { StudentDTO } from '@dwengo-1/common/interfaces/student'; import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import {mapToClassDTO} from "./class";
export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group {
const assignmentDto = groupDto.assignment as AssignmentDTO; const assignmentDto = groupDto.assignment as AssignmentDTO;
@ -19,7 +22,8 @@ export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group {
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),
}; };
@ -27,7 +31,8 @@ export function mapToGroupDTO(group: Group): GroupDTO {
export function mapToGroupDTOId(group: Group): GroupDTO { export function mapToGroupDTOId(group: Group): GroupDTO {
return { return {
assignment: mapToAssignmentDTOId(group.assignment), class: group.assignment.within.classId!,
assignment: group.assignment.id!,
groupNumber: group.groupNumber!, groupNumber: group.groupNumber!,
}; };
} }
@ -37,6 +42,7 @@ export function mapToGroupDTOId(group: Group): GroupDTO {
*/ */
export function mapToShallowGroupDTO(group: Group): GroupDTO { export function mapToShallowGroupDTO(group: Group): GroupDTO {
return { 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,10 +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'; 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,
@ -12,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.
*/ */

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 {
@ -29,16 +32,14 @@ export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId {
}; };
} }
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission { export function mapToSubmission(submissionDTO: SubmissionDTO, submitter: Student, onBehalfOf: Group | undefined): 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,
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

@ -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,105 +1,90 @@
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, mapToShallowGroupDTO } 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 mapToShallowGroupDTO(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);
} }
@ -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,15 +1,17 @@
import { getAnswerRepository, getAssignmentRepository, getClassRepository, getGroupRepository, 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 { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
import { mapToAssignment } from '../interfaces/assignment.js'; 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( export async function getQuestionsAboutLearningObjectInAssignment(
loId: LearningObjectIdentifier, loId: LearningObjectIdentifier,
@ -32,10 +34,6 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea
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);
} }
@ -43,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);
} }
@ -85,53 +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,
const clazz = await getClassRepository().findById((questionDTO.inGroup.assignment as AssignmentDTO).class); });
const assignment = mapToAssignment(questionDTO.inGroup.assignment as AssignmentDTO, clazz!);
const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionDTO.inGroup.groupNumber);
try {
await questionRepository.createQuestion({
loId,
author,
inGroup: inGroup!,
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

@ -23,6 +23,7 @@ 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'; 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[]> {
@ -137,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,61 +1,56 @@
import { getAssignmentRepository, 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 { Language } from '@dwengo-1/common/util/language'; import { fetchStudent } from './students.js';
import { getExistingGroupFromGroupDTO } from './groups.js';
export async function getSubmission( import { Submission } from '../entities/assignments/submission.entity.js';
learningObjectHruid: string, import {Language} from "@dwengo-1/common/util/language";
language: Language,
version: number,
submissionNumber: number
): Promise<SubmissionDTO | null> {
const loId = new LearningObjectIdentifier(learningObjectHruid, language, version);
export async function fetchSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission> {
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 = submissionDTO.group ? await getExistingGroupFromGroupDTO(submissionDTO.group) : undefined;
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. * Returns all the submissions made by on behalf of any group the given student is in.
*/ */

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();

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

@ -53,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!);

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

@ -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

@ -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

@ -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];
} }

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

@ -0,0 +1,72 @@
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';
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 = groups.slice(0, 3);
assignments[1].groups = 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);
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,7 +1,9 @@
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,17 +1,17 @@
import { LearningObjectIdentifier } from './learning-content'; import { LearningObjectIdentifierDTO } from './learning-content';
import { StudentDTO } from './student'; import { StudentDTO } from './student';
import { GroupDTO } from './group'; import { GroupDTO } from './group';
export interface QuestionDTO { export interface QuestionDTO {
learningObjectIdentifier: LearningObjectIdentifier; learningObjectIdentifier: LearningObjectIdentifierDTO;
sequenceNumber?: number; sequenceNumber?: number;
author: StudentDTO; author: StudentDTO;
inGroup: GroupDTO; inGroup: GroupDTO;
timestamp?: string; timestamp: string;
content: string; content: string;
} }
export interface QuestionId { export interface QuestionId {
learningObjectIdentifier: LearningObjectIdentifier; learningObjectIdentifier: LearningObjectIdentifierDTO;
sequenceNumber: number; sequenceNumber: number;
} }

View file

@ -1,10 +1,10 @@
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;

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

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éé"
} }

View file

@ -29,6 +29,11 @@
"sociallyRelevant": "Maatschappelijk relevant", "sociallyRelevant": "Maatschappelijk relevant",
"login": "log in", "login": "log in",
"translate": "vertalen", "translate": "vertalen",
"joinClass": "Word lid van een klas",
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
"invalidFormat": "Ongeldig formaat.",
"submitCode": "verzenden",
"members": "leden",
"themes": "Lesthema's", "themes": "Lesthema's",
"choose-theme": "Kies een thema", "choose-theme": "Kies een thema",
"choose-age": "Kies een leeftijd", "choose-age": "Kies een leeftijd",
@ -51,5 +56,24 @@
"high-school": "3e graad secundair", "high-school": "3e graad secundair",
"older": "Hoger onderwijs" "older": "Hoger onderwijs"
}, },
"read-more": "Lees meer" "read-more": "Lees meer",
"code": "code",
"class": "klas",
"invitations": "uitnodigingen",
"createClass": "klas aanmaken",
"createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.",
"classname": "klasnaam",
"EnterNameOfClass": "Geef een klasnaam op.",
"create": "aanmaken",
"sender": "afzender",
"nameIsMandatory": "klasnaam is verplicht",
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
"close": "sluiten",
"copied": "gekopieerd!",
"accept": "accepteren",
"deny": "weigeren",
"sent": "verzonden",
"failed": "mislukt",
"wrong": "er ging iets verkeerd",
"created": "gecreëerd"
} }

View file

@ -0,0 +1,51 @@
import type { QuestionId } from "@dwengo-1/common/dist/interfaces/question.ts";
import { type MaybeRefOrGetter, toValue } from "vue";
import { useMutation, type UseMutationReturnType, useQuery, type UseQueryReturnType } from "@tanstack/vue-query";
import { AnswerController, type AnswerResponse, type AnswersResponse } from "@/controllers/answers.ts";
import type { AnswerData } from "@dwengo-1/common/dist/interfaces/answer.ts";
// TODO caching
export function useAnswersQuery(
questionId: MaybeRefOrGetter<QuestionId>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<AnswersResponse, Error> {
return useQuery({
queryFn: async () => new AnswerController(toValue(questionId)).getAll(toValue(full)),
enabled: () => Boolean(toValue(questionId)),
});
}
export function useAnswerQuery(
questionId: MaybeRefOrGetter<QuestionId>,
sequenceNumber: MaybeRefOrGetter<number>,
): UseQueryReturnType<AnswerResponse, Error> {
return useQuery({
queryFn: async () => new AnswerController(toValue(questionId)).getBy(toValue(sequenceNumber)),
enabled: () => Boolean(toValue(questionId)),
});
}
export function useCreateAnswerMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<AnswerResponse, Error, AnswerData, unknown> {
return useMutation({
mutationFn: async (data) => new AnswerController(toValue(questionId)).create(data),
});
}
export function useDeleteAnswerMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<AnswerResponse, Error, number, unknown> {
return useMutation({
mutationFn: async (seq) => new AnswerController(toValue(questionId)).remove(seq),
});
}
export function useUpdateAnswerMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<AnswerResponse, Error, { answerData: AnswerData; seq: number }, unknown> {
return useMutation({
mutationFn: async (data, seq) => new AnswerController(toValue(questionId)).update(seq, data),
});
}

View file

@ -0,0 +1,93 @@
import { QuestionController, type QuestionResponse, type QuestionsResponse } from "@/controllers/questions.ts";
import type { QuestionData, QuestionId } from "@dwengo-1/common/interfaces/question";
import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content";
import { computed, type MaybeRefOrGetter, toValue } from "vue";
import {
useMutation,
type UseMutationReturnType,
useQuery,
useQueryClient,
type UseQueryReturnType,
} from "@tanstack/vue-query";
export function questionsQueryKey(
loId: LearningObjectIdentifierDTO,
full: boolean,
): [string, string, number, string, boolean] {
return ["questions", loId.hruid, loId.version, loId.language, full];
}
export function questionQueryKey(questionId: QuestionId): [string, string, number, string, number] {
const loId = questionId.learningObjectIdentifier;
return ["question", loId.hruid, loId.version, loId.language, questionId.sequenceNumber];
}
export function useQuestionsQuery(
loId: MaybeRefOrGetter<LearningObjectIdentifierDTO>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<QuestionsResponse, Error> {
return useQuery({
queryKey: computed(() => questionsQueryKey(toValue(loId), toValue(full))),
queryFn: async () => new QuestionController(toValue(loId)).getAll(toValue(full)),
enabled: () => Boolean(toValue(loId)),
});
}
export function useQuestionQuery(
questionId: MaybeRefOrGetter<QuestionId>,
): UseQueryReturnType<QuestionResponse, Error> {
const loId = toValue(questionId).learningObjectIdentifier;
const sequenceNumber = toValue(questionId).sequenceNumber;
return useQuery({
queryKey: computed(() => questionQueryKey(loId, sequenceNumber)),
queryFn: async () => new QuestionController(loId).getBy(sequenceNumber),
enabled: () => Boolean(toValue(questionId)),
});
}
export function useCreateQuestionMutation(
loId: MaybeRefOrGetter<LearningObjectIdentifierDTO>,
): UseMutationReturnType<QuestionResponse, Error, QuestionData, unknown> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data) => new QuestionController(toValue(loId)).create(data),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
},
});
}
export function useUpdateQuestionMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<QuestionResponse, Error, QuestionData, unknown> {
const queryClient = useQueryClient();
const loId = toValue(questionId).learningObjectIdentifier;
const sequenceNumber = toValue(questionId).sequenceNumber;
return useMutation({
mutationFn: async (data) => new QuestionController(loId).update(sequenceNumber, data),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) });
},
});
}
export function useDeleteQuestionMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<QuestionResponse, Error, void, unknown> {
const queryClient = useQueryClient();
const loId = toValue(questionId).learningObjectIdentifier;
const sequenceNumber = toValue(questionId).sequenceNumber;
return useMutation({
mutationFn: async () => new QuestionController(loId).remove(sequenceNumber),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) });
},
});
}

View file

@ -19,7 +19,7 @@ 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";
const studentController = new StudentController(); const studentController = new StudentController();
@ -179,7 +179,7 @@ export function useCreateJoinRequestMutation(): UseMutationReturnType<
mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId), mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId),
onSuccess: async (newJoinRequest) => { onSuccess: async (newJoinRequest) => {
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester), queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester.username),
}); });
}, },
}); });
@ -196,7 +196,7 @@ export function useDeleteJoinRequestMutation(): UseMutationReturnType<
return useMutation({ return useMutation({
mutationFn: async ({ username, classId }) => studentController.deleteJoinRequest(username, classId), mutationFn: async ({ username, classId }) => studentController.deleteJoinRequest(username, classId),
onSuccess: async (deletedJoinRequest) => { onSuccess: async (deletedJoinRequest) => {
const username = deletedJoinRequest.request.requester; const username = deletedJoinRequest.request.requester.username;
const classId = deletedJoinRequest.request.class; const classId = deletedJoinRequest.request.class;
await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) });
await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) }); await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) });

View file

@ -1,11 +1,17 @@
import { computed, toValue } from "vue"; import { computed, toValue } from "vue";
import type { MaybeRefOrGetter } from "vue"; import type { MaybeRefOrGetter } from "vue";
import { useMutation, useQuery, useQueryClient, UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query"; import {
useMutation,
useQuery,
useQueryClient,
type UseMutationReturnType,
type UseQueryReturnType,
} from "@tanstack/vue-query";
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts"; import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
import type { ClassesResponse } from "@/controllers/classes.ts"; import type { ClassesResponse } from "@/controllers/classes.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 { TeacherDTO } from "@dwengo-1/interfaces/teacher"; import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts"; import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
const teacherController = new TeacherController(); const teacherController = new TeacherController();

View file

@ -1,7 +1,224 @@
<script setup lang="ts"></script> <script setup lang="ts">
import { useI18n } from "vue-i18n";
import authState from "@/services/auth/auth-service.ts";
import { onMounted, ref } from "vue";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import { useRoute } from "vue-router";
import { ClassController, type ClassResponse } from "@/controllers/classes";
import type { StudentsResponse } from "@/controllers/students";
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
const { t } = useI18n();
// Username of logged in teacher
const username = ref<string | undefined>(undefined);
const classController: ClassController = new ClassController();
// Find class id from route
const route = useRoute();
const classId: string = route.params.id as string;
const isLoading = ref(true);
const currentClass = ref<ClassDTO | undefined>(undefined);
const students = ref<StudentDTO[]>([]);
// Find the username of the logged in user so it can be used to fetch other information
// When loading the page
onMounted(async () => {
const userObject = await authState.loadUser();
username.value = userObject?.profile?.preferred_username ?? undefined;
// Get class of which information should be shown
const classResponse: ClassResponse = await classController.getById(classId);
if (classResponse && classResponse.class) {
currentClass.value = classResponse.class;
isLoading.value = false;
}
// Fetch all students of the class
const studentsResponse: StudentsResponse = await classController.getStudents(classId);
if (studentsResponse && studentsResponse.students) students.value = studentsResponse.students as StudentDTO[];
});
// TODO: Boolean that handles visibility for dialogs
// Popup to verify removing student
const dialog = ref(false);
const selectedStudent = ref<StudentDTO | null>(null);
function showPopup(s: StudentDTO): void {
selectedStudent.value = s;
dialog.value = true;
}
// Remove student from class
function removeStudentFromclass(): void {
dialog.value = false;
}
</script>
<template> <template>
<main></main> <main>
</template> <div
v-if="isLoading"
class="text-center py-10"
>
<v-progress-circular
indeterminate
color="primary"
/>
<p>Loading...</p>
</div>
<div v-else>
<h1 class="title">{{ currentClass!.displayName }}</h1>
<v-container
fluid
class="ma-4"
>
<v-row
no-gutters
fluid
>
<v-col
cols="12"
sm="6"
md="6"
>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("students") }}</th>
<th class="header"></th>
</tr>
</thead>
<tbody>
<tr
v-for="s in students"
:key="s.id"
>
<td>
{{ s.firstName + " " + s.lastName }}
</td>
<td>
<v-btn @click="showPopup"> {{ t("remove") }} </v-btn>
</td>
</tr>
</tbody>
</v-table>
</v-col>
</v-row>
</v-container>
</div>
<v-dialog
v-model="dialog"
max-width="400px"
>
<v-card>
<v-card-title class="headline">{{ t("areusure") }}</v-card-title>
<style scoped></style> <v-card-actions>
<v-spacer></v-spacer>
<v-btn
text
@click="dialog = false"
>
{{ t("cancel") }}
</v-btn>
<v-btn
text
@click="removeStudentFromclass"
>{{ t("yes") }}</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
</main>
</template>
<style scoped>
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
table thead th:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-radius: 10px;
}
.table tbody tr:nth-child(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
h1 {
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
padding-top: 2%;
font-size: 50px;
}
h2 {
color: #0e6942;
font-size: 30px;
}
.join {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 50px;
}
.link {
color: #0b75bb;
text-decoration: underline;
}
main {
margin-left: 30px;
}
@media screen and (max-width: 800px) {
h1 {
text-align: center;
padding-left: 0;
}
.join {
text-align: center;
align-items: center;
margin-left: 0;
}
.sheet {
width: 100%;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 5px;
}
}
</style>

View file

@ -1,7 +1,380 @@
<script setup lang="ts"></script> <script setup lang="ts">
import { useI18n } from "vue-i18n";
import authState from "@/services/auth/auth-service.ts";
import { computed, onMounted, ref, type ComputedRef } from "vue";
import { validate, version } from "uuid";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students";
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
import { StudentController } from "@/controllers/students";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
import { TeacherController } from "@/controllers/teachers";
const { t } = useI18n();
const studentController: StudentController = new StudentController();
const teacherController: TeacherController = new TeacherController();
// Username of logged in student
const username = ref<string | undefined>(undefined);
// Find the username of the logged in user so it can be used to fetch other information
// When loading the page
onMounted(async () => {
const userObject = await authState.loadUser();
username.value = userObject?.profile?.preferred_username ?? undefined;
});
// Fetch all classes of the logged in student
const { data: classesResponse, isLoading, error } = useStudentClassesQuery(username);
// Empty list when classes are not yet loaded, else the list of classes of the user
const classes: ComputedRef<ClassDTO[]> = computed(() => {
// The classes are not yet fetched
if (!classesResponse.value) {
return [];
}
// The user has no classes
if (classesResponse.value.classes.length === 0) {
return [];
}
return classesResponse.value.classes as ClassDTO[];
});
// Students of selected class are shown when logged in student presses on the member count
const selectedClass = ref<ClassDTO | null>(null);
const students = ref<StudentDTO[]>([]);
const teachers = ref<TeacherDTO[]>([]);
const getStudents = ref(false);
// Boolean that handles visibility for dialogs
// Clicking on membercount will show a dialog with all members
const dialog = ref(false);
// Function to display all members of a class in a dialog
async function openStudentDialog(c: ClassDTO): Promise<void> {
selectedClass.value = c;
// Clear previous value
getStudents.value = true;
students.value = [];
dialog.value = true;
// Fetch students from their usernames to display their full names
const studentDTOs: (StudentDTO | null)[] = await Promise.all(
c.students.map(async (uid) => {
try {
const res = await studentController.getByUsername(uid);
return res.student;
} catch (_) {
return null;
}
}),
);
// Only show students that are not fetched ass *null*
students.value = studentDTOs.filter(Boolean) as StudentDTO[];
}
async function openTeacherDialog(c: ClassDTO): Promise<void> {
selectedClass.value = c;
// Clear previous value
getStudents.value = false;
teachers.value = [];
dialog.value = true;
// Fetch names of teachers
const teacherDTOs: (TeacherDTO | null)[] = await Promise.all(
c.teachers.map(async (uid) => {
try {
const res = await teacherController.getByUsername(uid);
return res.teacher;
} catch (_) {
return null;
}
}),
);
teachers.value = teacherDTOs.filter(Boolean) as TeacherDTO[];
}
// Hold the code a student gives in to join a class
const code = ref<string>("");
// The code a student sends in to join a class needs to be formatted as v4 to be valid
// These rules are used to display a message to the user if they use a code that has an invalid format
const codeRules = [
(value: string | undefined): string | boolean => {
if (value === undefined || value === "") {
return true;
} else if (value !== undefined && validate(value) && version(value) === 4) {
return true;
}
return t("invalidFormat");
},
];
// Used to send the actual class join request
const { mutate } = useCreateJoinRequestMutation();
// Function called when a student submits a code to join a class
function submitCode(): void {
// Check if the code is valid
if (code.value !== undefined && validate(code.value) && version(code.value) === 4) {
mutate(
{ username: username.value!, classId: code.value },
{
onSuccess: () => {
showSnackbar(t("sent"), "success");
},
onError: (e) => {
showSnackbar(t("failed") + ": " + e.message, "error");
},
},
);
code.value = "";
}
}
const snackbar = ref({
visible: false,
message: "",
color: "success",
});
function showSnackbar(message: string, color: string): void {
snackbar.value.message = message;
snackbar.value.color = color;
snackbar.value.visible = true;
}
</script>
<template> <template>
<main></main> <main>
</template> <div
v-if="isLoading"
class="text-center py-10"
>
<v-progress-circular
indeterminate
color="primary"
/>
<p>Loading...</p>
</div>
<style scoped></style> <div
v-else-if="error"
class="text-center py-10 text-error"
>
<v-icon large>mdi-alert-circle</v-icon>
<p>Error loading: {{ error.message }}</p>
</div>
<div v-else>
<h1 class="title">{{ t("classes") }}</h1>
<v-container
fluid
class="ma-4"
>
<v-row
no-gutters
fluid
>
<v-col
cols="12"
sm="6"
md="6"
>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("classes") }}</th>
<th class="header">{{ t("teachers") }}</th>
<th class="header">{{ t("members") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="c in classes"
:key="c.id"
>
<td>{{ c.displayName }}</td>
<td
class="link"
@click="openTeacherDialog(c)"
>
{{ c.teachers.length }}
</td>
<td
class="link"
@click="openStudentDialog(c)"
>
{{ c.students.length }}
</td>
</tr>
</tbody>
</v-table>
</v-col>
</v-row>
</v-container>
<v-dialog
v-model="dialog"
width="400"
>
<v-card>
<v-card-title> {{ selectedClass?.displayName }} </v-card-title>
<v-card-text>
<ul v-if="getStudents">
<li
v-for="student in students"
:key="student.username"
>
{{ student.firstName + " " + student.lastName }}
</li>
</ul>
<ul v-else>
<li
v-for="teacher in teachers"
:key="teacher.username"
>
{{ teacher.firstName + " " + teacher.lastName }}
</li>
</ul>
</v-card-text>
<v-card-actions>
<v-btn
color="primary"
@click="dialog = false"
>Close</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
<div>
<div class="join">
<h2>{{ t("joinClass") }}</h2>
<p>{{ t("JoinClassExplanation") }}</p>
<v-sheet
class="pa-4 sheet"
max-width="400"
>
<v-form @submit.prevent>
<v-text-field
label="CODE"
v-model="code"
placeholder="XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX"
:rules="codeRules"
variant="outlined"
></v-text-field>
<v-btn
class="mt-4"
color="#f6faf2"
type="submit"
@click="submitCode"
block
>{{ t("submitCode") }}</v-btn
>
</v-form>
</v-sheet>
</div>
</div>
</div>
<v-snackbar
v-model="snackbar.visible"
:color="snackbar.color"
timeout="3000"
>
{{ snackbar.message }}
</v-snackbar>
</main>
</template>
<style scoped>
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
table thead th:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-radius: 10px;
}
.table tbody tr:nth-child(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
h1 {
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
padding-top: 2%;
font-size: 50px;
}
h2 {
color: #0e6942;
font-size: 30px;
}
.join {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 50px;
}
.link {
color: #0b75bb;
text-decoration: underline;
}
main {
margin-left: 30px;
}
@media screen and (max-width: 800px) {
h1 {
text-align: center;
padding-left: 0;
}
.join {
text-align: center;
align-items: center;
margin-left: 0;
}
.sheet {
width: 100%;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 5px;
}
}
</style>

View file

@ -1,7 +1,404 @@
<script setup lang="ts"></script> <script setup lang="ts">
import { useI18n } from "vue-i18n";
import authState from "@/services/auth/auth-service.ts";
import { computed, onMounted, ref, type ComputedRef } from "vue";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
import { useTeacherClassesQuery } from "@/queries/teachers";
import { ClassController, type ClassResponse } from "@/controllers/classes";
const { t } = useI18n();
const classController = new ClassController();
// Username of logged in teacher
const username = ref<string | undefined>(undefined);
// Find the username of the logged in user so it can be used to fetch other information
// When loading the page
onMounted(async () => {
const userObject = await authState.loadUser();
username.value = userObject?.profile?.preferred_username ?? undefined;
});
// Fetch all classes of the logged in teacher
const { data: classesResponse, isLoading, error, refetch } = useTeacherClassesQuery(username, true);
// Empty list when classes are not yet loaded, else the list of classes of the user
const classes: ComputedRef<ClassDTO[]> = computed(() => {
// The classes are not yet fetched
if (!classesResponse.value) {
return [];
}
// The user has no classes
if (classesResponse.value.classes.length === 0) {
return [];
}
return classesResponse.value.classes as ClassDTO[];
});
// Boolean that handles visibility for dialogs
// Creating a class will generate a popup with the generated code
const dialog = ref(false);
// Code generated when new class was created
const code = ref<string>("");
// TODO: waiting on frontend controllers
const invitations = ref<TeacherInvitationDTO[]>([]);
// Function to handle a accepted invitation request
function acceptRequest(): void {
//TODO: avoid linting issues when merging by filling the function
invitations.value = [];
}
// Function to handle a denied invitation request
function denyRequest(): void {
//TODO: avoid linting issues when merging by filling the function
invitations.value = [];
}
// Teacher should be able to set a displayname when making a class
const className = ref<string>("");
// The name can only contain dash, underscore letters and numbers
// These rules are used to display a message to the user if the name is not valid
const nameRules = [
(value: string | undefined): string | boolean => {
if (!value) return true;
if (value && /^[a-zA-Z0-9_-]+$/.test(value)) return true;
return t("onlyUse");
},
];
// Function called when a teacher creates a class
async function createClass(): Promise<void> {
// Check if the class name is valid
if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) {
try {
const classDto: ClassDTO = {
id: "",
displayName: className.value,
teachers: [username.value!],
students: [],
joinRequests: [],
};
const classResponse: ClassResponse = await classController.createClass(classDto);
const createdClass: ClassDTO = classResponse.class;
code.value = createdClass.id;
dialog.value = true;
showSnackbar(t("created"), "success");
// Reload the table with classes so the new class appears
await refetch();
} catch (_) {
showSnackbar(t("wrong"), "error");
}
}
if (!className.value || className.value === "") {
showSnackbar(t("name is mandatory"), "error");
}
}
const snackbar = ref({
visible: false,
message: "",
color: "success",
});
function showSnackbar(message: string, color: string): void {
snackbar.value.message = message;
snackbar.value.color = color;
snackbar.value.visible = true;
}
// Show the teacher, copying of the code was a successs
const copied = ref(false);
// Copy the generated code to the clipboard
async function copyToClipboard(): Promise<void> {
await navigator.clipboard.writeText(code.value);
copied.value = true;
}
</script>
<template> <template>
<main></main> <main>
</template> <div
v-if="isLoading"
class="text-center py-10"
>
<v-progress-circular
indeterminate
color="primary"
/>
<p>Loading...</p>
</div>
<style scoped></style> <div
v-else-if="error"
class="text-center py-10 text-error"
>
<v-icon large>mdi-alert-circle</v-icon>
<p>Error loading: {{ error.message }}</p>
</div>
<div v-else>
<h1 class="title">{{ t("classes") }}</h1>
<v-container
fluid
class="ma-4"
>
<v-row
no-gutters
fluid
>
<v-col
cols="12"
sm="6"
md="6"
>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("classes") }}</th>
<th class="header">
{{ t("code") }}
</th>
<th class="header">{{ t("members") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="c in classes"
:key="c.id"
>
<td>
<v-btn
:to="`/class/${c.id}`"
variant="text"
>
{{ c.displayName }}
<v-icon end> mdi-menu-right </v-icon>
</v-btn>
</td>
<td>{{ c.id }}</td>
<td>{{ c.students.length }}</td>
</tr>
</tbody>
</v-table>
</v-col>
<v-col
cols="12"
sm="6"
md="6"
>
<div>
<h2>{{ t("createClass") }}</h2>
<v-sheet
class="pa-4 sheet"
max-width="600px"
>
<p>{{ t("createClassInstructions") }}</p>
<v-form @submit.prevent>
<v-text-field
class="mt-4"
:label="`${t('classname')}`"
v-model="className"
:placeholder="`${t('EnterNameOfClass')}`"
:rules="nameRules"
variant="outlined"
></v-text-field>
<v-btn
class="mt-4"
color="#f6faf2"
type="submit"
@click="createClass"
block
>{{ t("create") }}</v-btn
>
</v-form>
</v-sheet>
<v-container>
<v-dialog
v-model="dialog"
max-width="400px"
>
<v-card>
<v-card-title class="headline">code</v-card-title>
<v-card-text>
<v-text-field
v-model="code"
readonly
append-inner-icon="mdi-content-copy"
@click:append-inner="copyToClipboard"
></v-text-field>
<v-slide-y-transition>
<div
v-if="copied"
class="text-center mt-2"
>
{{ t("copied") }}
</div>
</v-slide-y-transition>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text
@click="
dialog = false;
copied = false;
"
>
{{ t("close") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</div>
</v-col>
</v-row>
</v-container>
<h1 class="title">
{{ t("invitations") }}
</h1>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("class") }}</th>
<th class="header">{{ t("sender") }}</th>
<th class="header"></th>
</tr>
</thead>
<tbody>
<tr
v-for="i in invitations"
:key="(i.class as ClassDTO).id"
>
<td>
{{ (i.class as ClassDTO).displayName }}
</td>
<td>{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}</td>
<td class="text-right">
<div>
<v-btn
color="green"
@click="acceptRequest"
class="mr-2"
>
{{ t("accept") }}
</v-btn>
<v-btn
color="red"
@click="denyRequest"
>
{{ t("deny") }}
</v-btn>
</div>
</td>
</tr>
</tbody>
</v-table>
</div>
<v-snackbar
v-model="snackbar.visible"
:color="snackbar.color"
timeout="3000"
>
{{ snackbar.message }}
</v-snackbar>
</main>
</template>
<style scoped>
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
table thead th:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-radius: 10px;
}
.table tbody tr:nth-child(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
h1 {
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
padding-top: 2%;
font-size: 50px;
}
h2 {
color: #0e6942;
font-size: 30px;
}
.join {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 50px;
}
.link {
color: #0b75bb;
text-decoration: underline;
}
main {
margin-left: 30px;
}
@media screen and (max-width: 800px) {
h1 {
text-align: center;
padding-left: 0;
}
.join {
text-align: center;
align-items: center;
margin-left: 0;
}
.sheet {
width: 100%;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 5px;
}
}
</style>

View file

@ -1,7 +1,17 @@
<script setup lang="ts"></script> <script setup lang="ts">
import authState from "@/services/auth/auth-service.ts";
import TeacherClasses from "./TeacherClasses.vue";
import StudentClasses from "./StudentClasses.vue";
// Determine if role is student or teacher to render correct view
const role: string = authState.authState.activeRole!;
</script>
<template> <template>
<main></main> <main>
<TeacherClasses v-if="role === 'teacher'"></TeacherClasses>
<StudentClasses v-else></StudentClasses>
</main>
</template> </template>
<style scoped></style> <style scoped></style>