diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts index 1520fc10..2ecb35cb 100644 --- a/backend/src/controllers/assignments.ts +++ b/backend/src/controllers/assignments.ts @@ -1,77 +1,94 @@ 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 { 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 -interface AssignmentParams { - classid: string; - id: string; -} - -export async function getAllAssignmentsHandler(req: Request, res: Response): Promise { - const classid = req.params.classid; +export async function getAllAssignmentsHandler(req: Request, res: Response): Promise { + const classId = req.params.classid; const full = req.query.full === 'true'; - const assignments = await getAllAssignments(classid, full); + const assignments = await getAllAssignments(classId, full); - res.json({ - assignments: assignments, - }); + res.json({ assignments }); } -export async function createAssignmentHandler(req: Request, res: Response): Promise { +export async function createAssignmentHandler(req: Request, res: Response): Promise { 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; - - if (!assignmentData.description || !assignmentData.language || !assignmentData.learningPath || !assignmentData.title) { - res.status(400).json({ - error: 'Missing one or more required fields: title, description, learningPath, language', - }); - return; - } - const assignment = await createAssignment(classid, assignmentData); - if (!assignment) { - res.status(500).json({ error: 'Could not create assignment ' }); - return; - } - - res.status(201).json(assignment); + res.json({ assignment }); } -export async function getAssignmentHandler(req: Request, res: Response): Promise { +export async function getAssignmentHandler(req: Request, res: Response): Promise { const id = Number(req.params.id); const classid = req.params.classid; + requireFields({ id, classid }); if (isNaN(id)) { - res.status(400).json({ error: 'Assignment id must be a number' }); - return; + throw new BadRequestException('Assignment id should be a number'); } const assignment = await getAssignment(classid, id); - if (!assignment) { - res.status(404).json({ error: 'Assignment not found' }); - return; - } - - res.json(assignment); + res.json({ assignment }); } -export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise { +export async function putAssignmentHandler(req: Request, res: Response): Promise { + 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>; + const assignment = await putAssignment(classid, id, assignmentData); + + res.json({ assignment }); +} + +export async function deleteAssignmentHandler(req: Request, _res: Response): Promise { + 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 { const classid = req.params.classid; const assignmentNumber = Number(req.params.id); const full = req.query.full === 'true'; + requireFields({ assignmentNumber, classid }); if (isNaN(assignmentNumber)) { - res.status(400).json({ error: 'Assignment id must be a number' }); - return; + throw new BadRequestException('Assignment id should be a number'); } const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full); - res.json({ - submissions: submissions, - }); + res.json({ submissions }); } diff --git a/backend/src/controllers/classes.ts b/backend/src/controllers/classes.ts index 03e88a52..6f253547 100644 --- a/backend/src/controllers/classes.ts +++ b/backend/src/controllers/classes.ts @@ -1,44 +1,62 @@ 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 { 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 { const full = req.query.full === 'true'; const classes = await getAllClasses(full); - res.json({ - classes: classes, - }); + res.json({ classes }); } export async function createClassHandler(req: Request, res: Response): Promise { + const displayName = req.body.displayName; + requireFields({ displayName }); + const classData = req.body as ClassDTO; - - if (!classData.displayName) { - res.status(400).json({ - error: 'Missing one or more required fields: displayName', - }); - return; - } - const cls = await createClass(classData); - if (!cls) { - res.status(500).json({ error: 'Something went wrong while creating class' }); - return; - } - - res.status(201).json({ class: cls }); + res.json({ class: cls }); } export async function getClassHandler(req: Request, res: Response): Promise { const classId = req.params.id; + requireFields({ classId }); + const cls = await getClass(classId); - if (!cls) { - res.status(404).json({ error: 'Class not found' }); - return; - } + res.json({ class: cls }); +} + +export async function putClassHandler(req: Request, res: Response): Promise { + const classId = req.params.id; + requireFields({ classId }); + + const newData = req.body as Partial>; + const cls = await putClass(classId, newData); + + res.json({ class: cls }); +} + +export async function deleteClassHandler(req: Request, res: Response): Promise { + const classId = req.params.id; + const cls = await deleteClass(classId); res.json({ class: cls }); } @@ -46,21 +64,69 @@ export async function getClassHandler(req: Request, res: Response): Promise { const classId = req.params.id; 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({ - students: students, - }); + res.json({ students }); +} + +export async function getClassTeachersHandler(req: Request, res: Response): Promise { + 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 { const classId = req.params.id; const full = req.query.full === 'true'; + requireFields({ classId }); const invitations = await getClassTeacherInvitations(classId, full); - res.json({ - invitations: invitations, - }); + res.json({ invitations }); +} + +export async function deleteClassStudentHandler(req: Request, res: Response): Promise { + 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 { + 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 { + 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 { + const classId = req.params.id; + const username = req.body.username; + requireFields({ classId, username }); + + const cls = await addClassTeacher(classId, username); + + res.json({ class: cls }); } diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts index 989066a6..ec177dcc 100644 --- a/backend/src/controllers/groups.ts +++ b/backend/src/controllers/groups.ts @@ -1,100 +1,104 @@ 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 { 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 -interface GroupParams { - classid: string; - assignmentid: string; - groupid?: string; -} - -export async function getGroupHandler(req: Request, res: Response): Promise { - const classId = req.params.classid; - const full = req.query.full === 'true'; - const assignmentId = Number(req.params.assignmentid); +function checkGroupFields(classId: string, assignmentId: number, groupId: number): void { + requireFields({ classId, assignmentId, groupId }); if (isNaN(assignmentId)) { - res.status(400).json({ error: 'Assignment id must be a number' }); - return; + throw new BadRequestException('Assignment id must be a number'); } - const groupId = Number(req.params.groupid!); // Can't be undefined - if (isNaN(groupId)) { - res.status(400).json({ error: 'Group id must be a number' }); - return; + throw new BadRequestException('Group id must be a number'); } +} - const group = await getGroup(classId, assignmentId, groupId, full); +export async function getGroupHandler(req: Request, res: Response): Promise { + const classId = req.params.classid; + const assignmentId = parseInt(req.params.assignmentid); + const groupId = parseInt(req.params.groupid); + checkGroupFields(classId, assignmentId, groupId); - if (!group) { - res.status(404).json({ error: 'Group not found' }); - return; - } + const group = await getGroup(classId, assignmentId, groupId); - res.json(group); + res.json({ group }); +} + +export async function putGroupHandler(req: Request, res: Response): Promise { + 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>); + + res.json({ group }); +} + +export async function deleteGroupHandler(req: Request, res: Response): Promise { + 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 { const classId = req.params.classid; - const full = req.query.full === 'true'; - const assignmentId = Number(req.params.assignmentid); + const full = req.query.full === 'true'; + requireFields({ classId, assignmentId }); if (isNaN(assignmentId)) { - res.status(400).json({ error: 'Assignment id must be a number' }); - return; + throw new BadRequestException('Assignment id must be a number'); } const groups = await getAllGroups(classId, assignmentId, full); - res.json({ - groups: groups, - }); + res.json({ groups }); } export async function createGroupHandler(req: Request, res: Response): Promise { const classid = req.params.classid; const assignmentId = Number(req.params.assignmentid); + requireFields({ classid, assignmentId }); + if (isNaN(assignmentId)) { - res.status(400).json({ error: 'Assignment id must be a number' }); - return; + throw new BadRequestException('Assignment id must be a number'); } const groupData = req.body as GroupDTO; const group = await createGroup(groupData, classid, assignmentId); - if (!group) { - res.status(500).json({ error: 'Something went wrong while creating group' }); - return; - } - - res.status(201).json(group); + res.status(201).json({ group }); } export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise { const classId = req.params.classid; + const assignmentId = Number(req.params.assignmentid); + const groupId = Number(req.params.groupid); const full = req.query.full === 'true'; - const assignmentId = Number(req.params.assignmentid); + requireFields({ classId, assignmentId, groupId }); if (isNaN(assignmentId)) { - res.status(400).json({ error: 'Assignment id must be a number' }); - return; + throw new BadRequestException('Assignment id must be a number'); } - const groupId = Number(req.params.groupid); // Can't be undefined - if (isNaN(groupId)) { - res.status(400).json({ error: 'Group id must be a number' }); - return; + throw new BadRequestException('Group id must be a number'); } const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full); - res.json({ - submissions: submissions, - }); + res.json({ submissions }); } diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index 239eb6d7..fb681656 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -1,61 +1,61 @@ import { Request, Response } from 'express'; -import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js'; -import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; +import { createSubmission, deleteSubmission, getAllSubmissions, getSubmission } from '../services/submissions.js'; +import { BadRequestException } from '../exceptions/bad-request-exception.js'; +import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { Language, languageMap } from '@dwengo-1/common/util/language'; +import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; +import { requireFields } from './error-helper.js'; -interface SubmissionParams { - hruid: string; - id: number; -} - -export async function getSubmissionHandler(req: Request, res: Response): Promise { +export async function getSubmissionHandler(req: Request, res: Response): Promise { 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 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 (!submission) { - res.status(404).json({ error: 'Submission not found' }); - return; + if (isNaN(submissionNumber)) { + throw new BadRequestException('Submission number must be a number'); } - 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 { + 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 { const submissionDTO = req.body as SubmissionDTO; - const submission = await createSubmission(submissionDTO); - if (!submission) { - res.status(400).json({ error: 'Failed to create submission' }); - return; - } - - res.json(submission); + res.json({ submission }); } export async function deleteSubmissionHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; - const submissionNumber = Number(req.params.id); - const lang = languageMap[req.query.language as string] || Language.Dutch; 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 (!submission) { - res.status(404).json({ error: 'Submission not found' }); - return; + if (isNaN(submissionNumber)) { + throw new BadRequestException('Submission number must be a number'); } - res.json(submission); + const loId = new LearningObjectIdentifier(hruid, lang, version); + const submission = await deleteSubmission(loId, submissionNumber); + + res.json({ submission }); } diff --git a/backend/src/controllers/teachers.ts b/backend/src/controllers/teachers.ts index 9275ca92..c8063f80 100644 --- a/backend/src/controllers/teachers.ts +++ b/backend/src/controllers/teachers.ts @@ -81,16 +81,15 @@ export async function getTeacherQuestionHandler(req: Request, res: Response): Pr } export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise { - const username = req.query.username as string; const classId = req.params.classId; - requireFields({ username, classId }); + requireFields({ classId }); const joinRequests = await getJoinRequestsByClass(classId); res.json({ joinRequests }); } export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise { - const studentUsername = req.query.studentUsername as string; + const studentUsername = req.params.studentUsername; const classId = req.params.classId; const accepted = req.body.accepted !== 'false'; // Default = true requireFields({ studentUsername, classId }); diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index f5090adc..2e673210 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -17,6 +17,14 @@ export class SubmissionRepository extends DwengoEntityRepository { }); } + public async findByLearningObject(loId: LearningObjectIdentifier): Promise { + return this.find({ + learningObjectHruid: loId.hruid, + learningObjectLanguage: loId.language, + learningObjectVersion: loId.version, + }); + } + public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise { return this.findOne( { diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index cf9ecab0..01e38df2 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -3,6 +3,7 @@ import { Question } from '../../entities/questions/question.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { Student } from '../../entities/users/student.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; +import { Assignment } from '../../entities/assignments/assignment.entity.js'; import { Loaded } from '@mikro-orm/core'; export class QuestionRepository extends DwengoEntityRepository { @@ -56,6 +57,14 @@ export class QuestionRepository extends DwengoEntityRepository { }); } + public async findAllByAssignment(assignment: Assignment): Promise { + return this.find({ + author: assignment.groups.flatMap((group) => group.members), + learningObjectHruid: assignment.learningPathHruid, + learningObjectLanguage: assignment.learningPathLanguage, + }); + } + public async findAllByAuthor(author: Student): Promise { return this.findAll({ where: { author }, diff --git a/backend/src/interfaces/assignment.ts b/backend/src/interfaces/assignment.ts index d48a9083..7abb3d3c 100644 --- a/backend/src/interfaces/assignment.ts +++ b/backend/src/interfaces/assignment.ts @@ -8,19 +8,18 @@ import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO { return { id: assignment.id!, - class: assignment.within.classId!, + within: assignment.within.classId!, title: assignment.title, description: assignment.description, learningPath: assignment.learningPathHruid, language: assignment.learningPathLanguage, - // Groups: assignment.groups.map(group => group.groupNumber), }; } export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO { return { id: assignment.id!, - class: assignment.within.classId!, + within: assignment.within.classId!, title: assignment.title, description: assignment.description, learningPath: assignment.learningPathHruid, diff --git a/backend/src/interfaces/class.ts b/backend/src/interfaces/class.ts index 7b07fcf2..76fa5fd5 100644 --- a/backend/src/interfaces/class.ts +++ b/backend/src/interfaces/class.ts @@ -10,7 +10,6 @@ export function mapToClassDTO(cls: Class): ClassDTO { displayName: cls.displayName, teachers: cls.teachers.map((teacher) => teacher.username), students: cls.students.map((student) => student.username), - joinRequests: [], // TODO }; } diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts index 1a169b2b..ac0efa64 100644 --- a/backend/src/interfaces/group.ts +++ b/backend/src/interfaces/group.ts @@ -1,11 +1,13 @@ import { Group } from '../entities/assignments/group.entity.js'; import { mapToAssignmentDTO } from './assignment.js'; +import { mapToClassDTO } from './class.js'; import { mapToStudentDTO } from './student.js'; import { GroupDTO } from '@dwengo-1/common/interfaces/group'; export function mapToGroupDTO(group: Group): GroupDTO { return { - assignment: mapToAssignmentDTO(group.assignment), // ERROR: , group.assignment.within), + class: mapToClassDTO(group.assignment.within), + assignment: mapToAssignmentDTO(group.assignment), groupNumber: group.groupNumber!, members: group.members.map(mapToStudentDTO), }; @@ -13,6 +15,7 @@ export function mapToGroupDTO(group: Group): GroupDTO { export function mapToGroupDTOId(group: Group): GroupDTO { return { + class: group.assignment.within.classId!, assignment: group.assignment.id!, groupNumber: group.groupNumber!, members: group.members.map((member) => member.username), diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts index b4ed4a2b..91882c35 100644 --- a/backend/src/interfaces/submission.ts +++ b/backend/src/interfaces/submission.ts @@ -1,6 +1,9 @@ +import { getSubmissionRepository } from '../data/repositories.js'; +import { Group } from '../entities/assignments/group.entity.js'; import { Submission } from '../entities/assignments/submission.entity.js'; +import { Student } from '../entities/users/student.entity.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'; export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { @@ -29,17 +32,14 @@ export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId { }; } -export function mapToSubmission(submissionDTO: SubmissionDTO): Submission { - const submission = new Submission(); - submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid; - submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language; - submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!; - // Submission.submissionNumber = submissionDTO.submissionNumber; - submission.submitter = mapToStudent(submissionDTO.submitter); - // Submission.submissionTime = submissionDTO.time; - // Submission.onBehalfOf = submissionDTO.group!; - // TODO fix group - submission.content = submissionDTO.content; - - return submission; +export function mapToSubmission(submissionDTO: SubmissionDTO, submitter: Student, onBehalfOf: Group | undefined): Submission { + return getSubmissionRepository().create({ + learningObjectHruid: submissionDTO.learningObjectIdentifier.hruid, + learningObjectLanguage: submissionDTO.learningObjectIdentifier.language, + learningObjectVersion: submissionDTO.learningObjectIdentifier.version || 1, + submitter: submitter, + submissionTime: new Date(), + content: submissionDTO.content, + onBehalfOf: onBehalfOf, + }); } diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts index 3652dcc6..083ee586 100644 --- a/backend/src/routes/assignments.ts +++ b/backend/src/routes/assignments.ts @@ -1,22 +1,26 @@ import express from 'express'; import { createAssignmentHandler, + deleteAssignmentHandler, getAllAssignmentsHandler, getAssignmentHandler, getAssignmentsSubmissionsHandler, + putAssignmentHandler, } from '../controllers/assignments.js'; import groupRouter from './groups.js'; const router = express.Router({ mergeParams: true }); -// Root endpoint used to search objects router.get('/', getAllAssignmentsHandler); router.post('/', createAssignmentHandler); -// Information about an assignment with id 'id' router.get('/:id', getAssignmentHandler); +router.put('/:id', putAssignmentHandler); + +router.delete('/:id', deleteAssignmentHandler); + router.get('/:id/submissions', getAssignmentsSubmissionsHandler); router.get('/:id/questions', (_req, res) => { diff --git a/backend/src/routes/classes.ts b/backend/src/routes/classes.ts index e0972988..cef6fd72 100644 --- a/backend/src/routes/classes.ts +++ b/backend/src/routes/classes.ts @@ -1,10 +1,17 @@ import express from 'express'; import { + addClassStudentHandler, + addClassTeacherHandler, createClassHandler, + deleteClassHandler, + deleteClassStudentHandler, + deleteClassTeacherHandler, getAllClassesHandler, getClassHandler, getClassStudentsHandler, + getClassTeachersHandler, getTeacherInvitationsHandler, + putClassHandler, } from '../controllers/classes.js'; import assignmentRouter from './assignments.js'; @@ -15,13 +22,26 @@ router.get('/', getAllClassesHandler); router.post('/', createClassHandler); -// Information about an class with id 'id' router.get('/:id', getClassHandler); +router.put('/:id', putClassHandler); + +router.delete('/:id', deleteClassHandler); + router.get('/:id/teacher-invitations', getTeacherInvitationsHandler); 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); export default router; diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts index 1486edce..7f973972 100644 --- a/backend/src/routes/groups.ts +++ b/backend/src/routes/groups.ts @@ -1,5 +1,12 @@ 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 }); @@ -8,16 +15,12 @@ router.get('/', getAllGroupsHandler); router.post('/', createGroupHandler); -// Information about a group (members, ... [TODO DOC]) router.get('/:groupid', getGroupHandler); +router.put('/:groupid', putGroupHandler); + +router.delete('/:groupid', deleteGroupHandler); + router.get('/:groupid/submissions', getGroupSubmissionsHandler); -// The list of questions a group has made -router.get('/:id/questions', (_req, res) => { - res.json({ - questions: ['0'], - }); -}); - export default router; diff --git a/backend/src/routes/submissions.ts b/backend/src/routes/submissions.ts index 8e9831b9..7c91de52 100644 --- a/backend/src/routes/submissions.ts +++ b/backend/src/routes/submissions.ts @@ -1,13 +1,9 @@ import express from 'express'; -import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler } from '../controllers/submissions.js'; +import { createSubmissionHandler, deleteSubmissionHandler, getAllSubmissionsHandler, getSubmissionHandler } from '../controllers/submissions.js'; const router = express.Router({ mergeParams: true }); // Root endpoint used to search objects -router.get('/', (_req, res) => { - res.json({ - submissions: ['0', '1'], - }); -}); +router.get('/', getAllSubmissionsHandler); router.post('/:id', createSubmissionHandler); diff --git a/backend/src/services/assignments.ts b/backend/src/services/assignments.ts index e86b69b2..5fd8f67f 100644 --- a/backend/src/services/assignments.ts +++ b/backend/src/services/assignments.ts @@ -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 { + 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 { 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 { +export async function fetchAssignment(classid: string, assignmentNumber: number): Promise { const classRepository = getClassRepository(); const cls = await classRepository.findById(classid); 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 { + const cls = await fetchClass(classid); + const assignmentRepository = getAssignmentRepository(); const assignments = await assignmentRepository.findAllAssignmentsInClass(cls); @@ -23,42 +48,37 @@ export async function getAllAssignments(classid: string, full: boolean): Promise return assignments.map(mapToAssignmentDTOId); } -export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise { - const classRepository = getClassRepository(); - const cls = await classRepository.findById(classid); - - if (!cls) { - return null; - } +export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise { + const cls = await fetchClass(classid); const assignment = mapToAssignment(assignmentData, cls); + const assignmentRepository = getAssignmentRepository(); + const newAssignment = assignmentRepository.create(assignment); + await assignmentRepository.save(newAssignment, { preventOverwrite: true }); - try { - const newAssignment = assignmentRepository.create(assignment); - await assignmentRepository.save(newAssignment); - - return mapToAssignmentDTO(newAssignment); - } catch (e) { - getLogger().error(e); - return null; - } + return mapToAssignmentDTO(newAssignment); } -export async function getAssignment(classid: string, id: number): Promise { - const classRepository = getClassRepository(); - const cls = await classRepository.findById(classid); +export async function getAssignment(classid: string, id: number): Promise { + const assignment = await fetchAssignment(classid, id); + return mapToAssignmentDTO(assignment); +} - if (!cls) { - return null; - } +export async function putAssignment(classid: string, id: number, assignmentData: Partial>): Promise { + const assignment = await fetchAssignment(classid, id); + + await putObject(assignment, assignmentData, getAssignmentRepository()); + + return mapToAssignmentDTO(assignment); +} + +export async function deleteAssignment(classid: string, id: number): Promise { + const assignment = await fetchAssignment(classid, id); + const cls = await fetchClass(classid); const assignmentRepository = getAssignmentRepository(); - const assignment = await assignmentRepository.findByClassAndId(cls, id); - - if (!assignment) { - return null; - } + await assignmentRepository.deleteByClassAndId(cls, id); return mapToAssignmentDTO(assignment); } @@ -68,19 +88,7 @@ export async function getAssignmentsSubmissions( assignmentNumber: number, full: boolean ): Promise { - const classRepository = getClassRepository(); - const cls = await classRepository.findById(classid); - - if (!cls) { - return []; - } - - const assignmentRepository = getAssignmentRepository(); - const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); - - if (!assignment) { - return []; - } + const assignment = await fetchAssignment(classid, assignmentNumber); const groupRepository = getGroupRepository(); const groups = await groupRepository.findAllGroupsForAssignment(assignment); @@ -94,3 +102,16 @@ export async function getAssignmentsSubmissions( return submissions.map(mapToSubmissionDTOId); } + +export async function getAssignmentsQuestions(classid: string, assignmentNumber: number, full: boolean): Promise { + 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); +} diff --git a/backend/src/services/classes.ts b/backend/src/services/classes.ts index 754277cf..1f197b2a 100644 --- a/backend/src/services/classes.ts +++ b/backend/src/services/classes.ts @@ -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 { mapToStudentDTO } from '../interfaces/student.js'; import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.js'; -import { getLogger } from '../logging/initalize.js'; import { NotFoundException } from '../exceptions/not-found-exception.js'; import { Class } from '../entities/classes/class.entity.js'; import { ClassDTO } from '@dwengo-1/common/interfaces/class'; import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; 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 { +export async function fetchClass(classid: string): Promise { const classRepository = getClassRepository(); - const cls = await classRepository.findById(classId); + const cls = await classRepository.findById(classid); if (!cls) { - throw new NotFoundException('Class with id not found'); + throw new NotFoundException('Class not found'); } return cls; @@ -24,11 +27,7 @@ export async function fetchClass(classId: string): Promise { export async function getAllClasses(full: boolean): Promise { const classRepository = getClassRepository(); - const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); - - if (!classes) { - return []; - } + const classes = await classRepository.findAll({ populate: ['students', 'teachers'] }); if (full) { return classes.map(mapToClassDTO); @@ -36,74 +35,71 @@ export async function getAllClasses(full: boolean): Promise cls.classId!); } -export async function createClass(classData: ClassDTO): Promise { - const teacherRepository = getTeacherRepository(); - const teacherUsernames = classData.teachers || []; - 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 { + const cls = await fetchClass(classId); + return mapToClassDTO(cls); } -export async function getClass(classId: string): Promise { - const classRepository = getClassRepository(); - const cls = await classRepository.findById(classId); +export async function createClass(classData: ClassDTO): Promise { + const teacherUsernames = classData.teachers || []; + const teachers = await Promise.all(teacherUsernames.map(async (id) => fetchTeacher(id))); - if (!cls) { - return null; - } + const studentUsernames = classData.students || []; + 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>): Promise { + const cls = await fetchClass(classId); + + await putObject(cls, classData, getClassRepository()); return mapToClassDTO(cls); } -async function fetchClassStudents(classId: string): Promise { +export async function deleteClass(classId: string): Promise { + const cls = await fetchClass(classId); + const classRepository = getClassRepository(); - const cls = await classRepository.findById(classId); + await classRepository.deleteById(classId); - if (!cls) { - return []; + return mapToClassDTO(cls); +} + +export async function getClassStudents(classId: string, full: boolean): Promise { + 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 { + const cls = await fetchClass(classId); return cls.students.map(mapToStudentDTO); } -export async function getClassStudents(classId: string): Promise { - return await fetchClassStudents(classId); -} +export async function getClassTeachers(classId: string, full: boolean): Promise { + const cls = await fetchClass(classId); -export async function getClassStudentsIds(classId: string): Promise { - const students: StudentDTO[] = await fetchClassStudents(classId); - return students.map((student) => student.username); + if (full) { + return cls.teachers.map(mapToTeacherDTO); + } + return cls.teachers.map((student) => student.username); } export async function getClassTeacherInvitations(classId: string, full: boolean): Promise { - const classRepository = getClassRepository(); - const cls = await classRepository.findById(classId); - - if (!cls) { - return []; - } + const cls = await fetchClass(classId); const teacherInvitationRepository = getTeacherInvitationRepository(); const invitations = await teacherInvitationRepository.findAllInvitationsForClass(cls); @@ -114,3 +110,41 @@ export async function getClassTeacherInvitations(classId: string, full: boolean) return invitations.map(mapToTeacherInvitationDTOIds); } + +export async function deleteClassStudent(classId: string, username: string): Promise { + const cls = await fetchClass(classId); + + const newStudents = { students: cls.students.filter((student) => student.username !== username) }; + await putObject(cls, newStudents, getClassRepository()); + + return mapToClassDTO(cls); +} + +export async function deleteClassTeacher(classId: string, username: string): Promise { + const cls = await fetchClass(classId); + + const newTeachers = { teachers: cls.teachers.filter((teacher) => teacher.username !== username) }; + await putObject(cls, newTeachers, getClassRepository()); + + return mapToClassDTO(cls); +} + +export async function addClassStudent(classId: string, username: string): Promise { + const cls = await fetchClass(classId); + const newStudent = await fetchStudent(username); + + const newStudents = { students: [...cls.students, newStudent] }; + await putObject(cls, newStudents, getClassRepository()); + + return mapToClassDTO(cls); +} + +export async function addClassTeacher(classId: string, username: string): Promise { + const cls = await fetchClass(classId); + const newTeacher = await fetchTeacher(username); + + const newTeachers = { teachers: [...cls.teachers, newTeacher] }; + await putObject(cls, newTeachers, getClassRepository()); + + return mapToClassDTO(cls); +} diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 346c1ee1..95b1b7d4 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -1,105 +1,90 @@ -import { - getAssignmentRepository, - getClassRepository, - getGroupRepository, - getStudentRepository, - getSubmissionRepository, -} from '../data/repositories.js'; +import { EntityDTO } from '@mikro-orm/core'; +import { getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; import { Group } from '../entities/assignments/group.entity.js'; import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; import { GroupDTO } from '@dwengo-1/common/interfaces/group'; 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 { - 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; - } +export async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise { + const assignment = await fetchAssignment(classId, assignmentNumber); const groupRepository = getGroupRepository(); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber); if (!group) { - return null; + throw new NotFoundException('Could not find group'); } - if (full) { - return mapToGroupDTO(group); - } - - return mapToGroupDTOId(group); + return group; } -export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise { +export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise { + const group = await fetchGroup(classId, assignmentNumber, groupNumber); + return mapToGroupDTO(group); +} + +export async function putGroup( + classId: string, + assignmentNumber: number, + groupNumber: number, + groupData: Partial> +): Promise { + const group = await fetchGroup(classId, assignmentNumber, groupNumber); + + await putObject(group, groupData, getGroupRepository()); + + return mapToGroupDTO(group); +} + +export async function deleteGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise { + 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 { + 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 { 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( (student) => student !== null ); - getLogger().debug(members); - - const classRepository = getClassRepository(); - const cls = await classRepository.findById(classid); - - if (!cls) { - return null; - } - - const assignmentRepository = getAssignmentRepository(); - const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); - - if (!assignment) { - return null; - } + const assignment = await fetchAssignment(classid, assignmentNumber); const groupRepository = getGroupRepository(); - try { - const newGroup = groupRepository.create({ - assignment: assignment, - members: members, - }); - await groupRepository.save(newGroup); + const newGroup = groupRepository.create({ + assignment: assignment, + members: members, + }); + await groupRepository.save(newGroup); - return newGroup; - } catch (e) { - getLogger().error(e); - return null; - } + return mapToGroupDTO(newGroup); } export async function getAllGroups(classId: string, assignmentNumber: number, full: boolean): Promise { - const classRepository = getClassRepository(); - const cls = await classRepository.findById(classId); - - if (!cls) { - return []; - } - - const assignmentRepository = getAssignmentRepository(); - const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); - - if (!assignment) { - return []; - } + const assignment = await fetchAssignment(classId, assignmentNumber); const groupRepository = getGroupRepository(); const groups = await groupRepository.findAllGroupsForAssignment(assignment); if (full) { - getLogger().debug({ full: full, groups: groups }); return groups.map(mapToGroupDTO); } @@ -112,26 +97,7 @@ export async function getGroupSubmissions( groupNumber: number, full: boolean ): Promise { - const classRepository = getClassRepository(); - const cls = await classRepository.findById(classId); - - if (!cls) { - return []; - } - - const assignmentRepository = getAssignmentRepository(); - const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); - - if (!assignment) { - return []; - } - - const groupRepository = getGroupRepository(); - const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber); - - if (!group) { - return []; - } + const group = await fetchGroup(classId, assignmentNumber, groupNumber); const submissionRepository = getSubmissionRepository(); const submissions = await submissionRepository.findAllSubmissionsForGroup(group); diff --git a/backend/src/services/service-helper.ts b/backend/src/services/service-helper.ts new file mode 100644 index 00000000..641fada4 --- /dev/null +++ b/backend/src/services/service-helper.ts @@ -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( + object: T, + data: Partial>>, + repo: DwengoEntityRepository +): Promise { + repo.assign(object, data); + await repo.getEntityManager().flush(); +} diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index dc40e468..6cfbbd5b 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -23,6 +23,7 @@ import { GroupDTO } from '@dwengo-1/common/interfaces/group'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; +import { ConflictException } from '../exceptions/conflict-exception.js'; export async function getAllStudents(full: boolean): Promise { const studentRepository = getStudentRepository(); @@ -135,6 +136,10 @@ export async function createClassJoinRequest(username: string, classId: string): const student = await fetchStudent(username); // Throws error if student not found const cls = await fetchClass(classId); + if (cls.students.contains(student)) { + throw new ConflictException('Student already in this class'); + } + const request = mapToStudentRequest(student, cls); await requestRepo.save(request, { preventOverwrite: true }); return mapToStudentRequestDTO(request); diff --git a/backend/src/services/submissions.ts b/backend/src/services/submissions.ts index 1d8a7874..c7daff74 100644 --- a/backend/src/services/submissions.ts +++ b/backend/src/services/submissions.ts @@ -1,57 +1,51 @@ import { getSubmissionRepository } from '../data/repositories.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 { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; -import { Language } from '@dwengo-1/common/util/language'; - -export async function getSubmission( - learningObjectHruid: string, - language: Language, - version: number, - submissionNumber: number -): Promise { - const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); +import { fetchStudent } from './students.js'; +import { getExistingGroupFromGroupDTO } from './groups.js'; +import { Submission } from '../entities/assignments/submission.entity.js'; +export async function fetchSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise { const submissionRepository = getSubmissionRepository(); const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber); if (!submission) { - return null; + throw new NotFoundException('Could not find submission'); } - return mapToSubmissionDTO(submission); -} - -export async function createSubmission(submissionDTO: SubmissionDTO): Promise { - 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 { - 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; } + +export async function getSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise { + const submission = await fetchSubmission(loId, submissionNumber); + return mapToSubmissionDTO(submission); +} + +export async function getAllSubmissions(loId: LearningObjectIdentifier): Promise { + const submissionRepository = getSubmissionRepository(); + const submissions = await submissionRepository.findByLearningObject(loId); + + return submissions.map(mapToSubmissionDTO); +} + +export async function createSubmission(submissionDTO: SubmissionDTO): Promise { + 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 { + const submission = await fetchSubmission(loId, submissionNumber); + + const submissionRepository = getSubmissionRepository(); + await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber); + + return mapToSubmissionDTO(submission); +} diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index 1b7643fb..e6596f9e 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -22,13 +22,14 @@ import { Question } from '../entities/questions/question.entity.js'; import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js'; import { Student } from '../entities/users/student.entity.js'; import { NotFoundException } from '../exceptions/not-found-exception.js'; -import { getClassStudents } from './classes.js'; +import { addClassStudent, fetchClass, getClassStudentsDTO } from './classes.js'; import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; import { ClassDTO } from '@dwengo-1/common/interfaces/class'; import { StudentDTO } from '@dwengo-1/common/interfaces/student'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; +import { ConflictException } from '../exceptions/conflict-exception.js'; export async function getAllTeachers(full: boolean): Promise { 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 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) { return students; } + return students.map((student) => student.username); } @@ -143,13 +146,12 @@ export async function getJoinRequestsByClass(classId: string): Promise { const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository(); - const classRepo: ClassRepository = getClassRepository(); const student: Student = await fetchStudent(studentUsername); - const cls: Class | null = await classRepo.findById(classId); + const cls = await fetchClass(classId); - if (!cls) { - throw new NotFoundException('Class not found'); + if (cls.students.contains(student)) { + throw new ConflictException('Student already in this class'); } 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'); } - 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); + return mapToStudentRequestDTO(request); } diff --git a/backend/tests/controllers/students.test.ts b/backend/tests/controllers/students.test.ts index 18331f2d..aca29de1 100644 --- a/backend/tests/controllers/students.test.ts +++ b/backend/tests/controllers/students.test.ts @@ -198,15 +198,34 @@ describe('Student controllers', () => { ); }); - it('Create join request', async () => { + it('Create and delete join request', async () => { req = { - params: { username: 'Noordkaap' }, + params: { username: 'TheDoors' }, body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, }; await createStudentRequestHandler(req as Request, res as Response); expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); + + req = { + params: { username: 'TheDoors', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, + }; + + await deleteClassJoinRequestHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); + + 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 () => { @@ -217,16 +236,4 @@ describe('Student controllers', () => { await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException); }); - - it('Delete join request', async () => { - req = { - params: { username: 'Noordkaap', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, - }; - - await deleteClassJoinRequestHandler(req as Request, res as Response); - - expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); - - await expect(async () => deleteClassJoinRequestHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); - }); }); diff --git a/backend/tests/controllers/teachers.test.ts b/backend/tests/controllers/teachers.test.ts index cdcb6229..a73a79a5 100644 --- a/backend/tests/controllers/teachers.test.ts +++ b/backend/tests/controllers/teachers.test.ts @@ -16,6 +16,7 @@ import { BadRequestException } from '../../src/exceptions/bad-request-exception. import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; import { getStudentRequestsHandler } from '../../src/controllers/students.js'; import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; +import { getClassHandler } from '../../src/controllers/classes'; describe('Teacher controllers', () => { let req: Partial; @@ -168,7 +169,6 @@ describe('Teacher controllers', () => { it('Get join requests by class', async () => { req = { - query: { username: 'LimpBizkit' }, params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, }; @@ -183,8 +183,7 @@ describe('Teacher controllers', () => { it('Update join request status', async () => { req = { - query: { username: 'LimpBizkit', studentUsername: 'PinkFloyd' }, - params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, + params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', studentUsername: 'PinkFloyd' }, body: { accepted: 'true' }, }; @@ -200,5 +199,13 @@ describe('Teacher controllers', () => { const status: boolean = jsonMock.mock.lastCall?.[0].requests[0].status; 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'); }); }); diff --git a/common/src/interfaces/assignment.ts b/common/src/interfaces/assignment.ts index 8ad1649b..5cb8feff 100644 --- a/common/src/interfaces/assignment.ts +++ b/common/src/interfaces/assignment.ts @@ -2,7 +2,7 @@ import { GroupDTO } from './group'; export interface AssignmentDTO { id: number; - class: string; // Id of class 'within' + within: string; title: string; description: string; learningPath: string; diff --git a/common/src/interfaces/class.ts b/common/src/interfaces/class.ts index c35c2dfc..d71e15e6 100644 --- a/common/src/interfaces/class.ts +++ b/common/src/interfaces/class.ts @@ -3,5 +3,4 @@ export interface ClassDTO { displayName: string; teachers: string[]; students: string[]; - joinRequests: string[]; } diff --git a/common/src/interfaces/group.ts b/common/src/interfaces/group.ts index ca95770a..742f2c75 100644 --- a/common/src/interfaces/group.ts +++ b/common/src/interfaces/group.ts @@ -1,7 +1,9 @@ import { AssignmentDTO } from './assignment'; +import { ClassDTO } from './class'; import { StudentDTO } from './student'; export interface GroupDTO { + class: string | ClassDTO; assignment: number | AssignmentDTO; groupNumber: number; members: string[] | StudentDTO[]; diff --git a/frontend/src/controllers/assignments.ts b/frontend/src/controllers/assignments.ts index 59ea2428..a66f8e84 100644 --- a/frontend/src/controllers/assignments.ts +++ b/frontend/src/controllers/assignments.ts @@ -33,6 +33,10 @@ export class AssignmentController extends BaseController { return this.delete(`/${num}`); } + async updateAssignment(num: number, data: Partial): Promise { + return this.put(`/${num}`, data); + } + async getSubmissions(assignmentNumber: number, full = true): Promise { return this.get(`/${assignmentNumber}/submissions`, { full }); } diff --git a/frontend/src/controllers/classes.ts b/frontend/src/controllers/classes.ts index c2a634eb..03e3f560 100644 --- a/frontend/src/controllers/classes.ts +++ b/frontend/src/controllers/classes.ts @@ -3,6 +3,7 @@ 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 { classes: ClassDTO[] | string[]; @@ -41,10 +42,34 @@ export class ClassController extends BaseController { return this.delete(`/${id}`); } + async updateClass(id: string, data: Partial): Promise { + return this.put(`/${id}`, data); + } + async getStudents(id: string, full = true): Promise { return this.get(`/${id}/students`, { full }); } + async addStudent(id: string, username: string): Promise { + return this.post(`/${id}/students`, { username }); + } + + async deleteStudent(id: string, username: string): Promise { + return this.delete(`/${id}/students/${username}`); + } + + async getTeachers(id: string, full = true): Promise { + return this.get(`/${id}/teachers`, { full }); + } + + async addTeacher(id: string, username: string): Promise { + return this.post(`/${id}/teachers`, { username }); + } + + async deleteTeacher(id: string, username: string): Promise { + return this.delete(`/${id}/teachers/${username}`); + } + async getTeacherInvitations(id: string, full = true): Promise { return this.get(`/${id}/teacher-invitations`, { full }); } diff --git a/frontend/src/controllers/groups.ts b/frontend/src/controllers/groups.ts index 05192636..de6592b5 100644 --- a/frontend/src/controllers/groups.ts +++ b/frontend/src/controllers/groups.ts @@ -32,6 +32,10 @@ export class GroupController extends BaseController { return this.delete(`/${num}`); } + async updateGroup(num: number, data: Partial): Promise { + return this.put(`/${num}`, data); + } + async getSubmissions(groupNumber: number, full = true): Promise { return this.get(`/${groupNumber}/submissions`, { full }); }