From 311e76149c1f27513bef421f3304e703c51a5092 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sat, 12 Apr 2025 17:55:39 +0200 Subject: [PATCH 01/13] feat: teacher invitation backend --- .../src/controllers/teacher-invitations.ts | 38 ++++++++++ .../classes/teacher-invitation-repository.ts | 7 ++ backend/src/interfaces/teacher-invitation.ts | 9 +++ backend/src/routes/teacher-invitations.ts | 17 +++++ backend/src/routes/teachers.ts | 8 +-- backend/src/services/teacher-invitations.ts | 71 +++++++++++++++++++ common/src/interfaces/teacher-invitation.ts | 6 ++ 7 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 backend/src/controllers/teacher-invitations.ts create mode 100644 backend/src/routes/teacher-invitations.ts create mode 100644 backend/src/services/teacher-invitations.ts diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts new file mode 100644 index 00000000..3292b7bf --- /dev/null +++ b/backend/src/controllers/teacher-invitations.ts @@ -0,0 +1,38 @@ +import { Request, Response } from 'express'; +import {requireFields} from "./error-helper"; +import {createInvitation, deleteInvitationFor, getAllInvitations} from "../services/teacher-invitations"; +import {TeacherInvitationData} from "@dwengo-1/common/interfaces/teacher-invitation"; + +export async function getAllInvitationsHandler(req: Request, res: Response): Promise { + const username = req.params.username; + const by = req.query.by === 'true'; + requireFields({ username }); + + const invitations = getAllInvitations(username, by); + + res.json({ invitations }); +} + +export async function createInvitationHandler(req: Request, res: Response): Promise { + const sender = req.body.sender; + const receiver = req.body.receiver; + const classId = req.body.class; + requireFields({ sender, receiver, classId }); + + const data = req.body as TeacherInvitationData; + const invitation = await createInvitation(data); + + res.json({ invitation }); +} + +export async function deleteInvitationForHandler(req: Request, res: Response): Promise { + const sender = req.params.sender; + const receiver = req.params.receiver; + const classId = req.params.class; + const accepted = req.body.accepted !== 'false'; + requireFields({ sender, receiver, classId }); + + const invitation = deleteInvitationFor(sender, receiver, classId, accepted); + + res.json({ invitation }); +} diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index ce059ca8..5461d29b 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -20,4 +20,11 @@ export class TeacherInvitationRepository extends DwengoEntityRepository { + return this.findOne({ + sender: sender, + receiver: receiver, + class: clazz, + }) + } } diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index d9cb9915..98189938 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -2,6 +2,9 @@ import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity import { mapToClassDTO } from './class.js'; import { mapToUserDTO } from './user.js'; import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; +import {getTeacherInvitationRepository} from "../data/repositories"; +import {Teacher} from "../entities/users/teacher.entity"; +import {Class} from "../entities/classes/class.entity"; export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { return { @@ -18,3 +21,9 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea class: invitation.class.classId!, }; } + +export function mapToInvitation(sender: Teacher, receiver: Teacher, cls: Class): TeacherInvitation { + return getTeacherInvitationRepository().create({ + sender, receiver, class: cls + }); +} diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts new file mode 100644 index 00000000..01a18195 --- /dev/null +++ b/backend/src/routes/teacher-invitations.ts @@ -0,0 +1,17 @@ +import express from "express"; +import { + createInvitationHandler, + deleteInvitationForHandler, + getAllInvitationsHandler +} from "../controllers/teacher-invitations"; + +const router = express.Router({ mergeParams: true }); + +router.get('/:username', getAllInvitationsHandler); + +router.post('/', createInvitationHandler); + +router.delete('/:sender/:receiver/:classId', deleteInvitationForHandler); + + +export default router; diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index a6106a80..801eaee8 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -10,6 +10,8 @@ import { getTeacherStudentHandler, updateStudentJoinRequestHandler, } from '../controllers/teachers.js'; +import invitationRouter from './teacher-invitations.js'; + const router = express.Router(); // Root endpoint used to search objects @@ -32,10 +34,6 @@ router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); // Invitations to other classes a teacher received -router.get('/:id/invitations', (_req, res) => { - res.json({ - invitations: ['0'], - }); -}); +router.get('/invitations', invitationRouter); export default router; diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts new file mode 100644 index 00000000..1b9ef179 --- /dev/null +++ b/backend/src/services/teacher-invitations.ts @@ -0,0 +1,71 @@ +import {fetchTeacher} from "./teachers"; +import {getTeacherInvitationRepository} from "../data/repositories"; +import {mapToInvitation, mapToTeacherInvitationDTO} from "../interfaces/teacher-invitation"; +import {addClassTeacher, fetchClass} from "./classes"; +import {TeacherInvitationData, TeacherInvitationDTO} from "@dwengo-1/common/interfaces/teacher-invitation"; +import {ConflictException} from "../exceptions/conflict-exception"; +import {Teacher} from "../entities/users/teacher.entity"; +import {Class} from "../entities/classes/class.entity"; +import {NotFoundException} from "../exceptions/not-found-exception"; +import {TeacherInvitation} from "../entities/classes/teacher-invitation.entity"; + +export async function getAllInvitations(username: string, by: boolean): Promise { + const teacher = await fetchTeacher(username); + const teacherInvitationRepository = getTeacherInvitationRepository(); + + let invitations; + if (by) { + invitations = await teacherInvitationRepository.findAllInvitationsBy(teacher); + } else { + invitations = await teacherInvitationRepository.findAllInvitationsFor(teacher); + } + return invitations.map(mapToTeacherInvitationDTO); +} + +export async function createInvitation(data: TeacherInvitationData): Promise { + const teacherInvitationRepository = getTeacherInvitationRepository(); + const sender = await fetchTeacher(data.sender); + const receiver = await fetchTeacher(data.receiver); + + const cls = await fetchClass(data.class); + + if (!cls.teachers.contains(sender)){ + throw new ConflictException("The teacher sending the invite is not part of the class"); + } + + const newInvitation = mapToInvitation(sender, receiver, cls); + await teacherInvitationRepository.save(newInvitation, {preventOverwrite: true}); + + return mapToTeacherInvitationDTO(newInvitation); +} + +async function fetchInvitation(sender: Teacher, receiver: Teacher, cls: Class): Promise { + const teacherInvitationRepository = getTeacherInvitationRepository(); + const invite = await teacherInvitationRepository.findBy(cls, sender, receiver); + + if (!invite){ + throw new NotFoundException("Teacher invite not found"); + } + + return invite; +} + +export async function deleteInvitationFor(usernameSender: string, usernameReceiver: string, classId: string, accepted: boolean) { + const teacherInvitationRepository = getTeacherInvitationRepository(); + const sender = await fetchTeacher(usernameSender); + const receiver = await fetchTeacher(usernameReceiver); + + const cls = await fetchClass(classId); + + const invitation = await fetchInvitation(sender, receiver, cls); + await teacherInvitationRepository.deleteBy(cls, sender, receiver); + + if (accepted){ + await addClassTeacher(classId, usernameReceiver); + } + + return mapToTeacherInvitationDTO(invitation); +} + + + diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index 13709322..c61f9a6a 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -6,3 +6,9 @@ export interface TeacherInvitationDTO { receiver: string | UserDTO; class: string | ClassDTO; } + +export interface TeacherInvitationData { + sender: string; + receiver: string; + class: string; +} From 5624f3bbfe9b93e05ba08ca0af9d465cab5da501 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sun, 13 Apr 2025 09:48:34 +0200 Subject: [PATCH 02/13] feat: tests --- .../src/controllers/teacher-invitations.ts | 6 +- .../controllers/teacher-invitations.test.ts | 97 +++++++++++++++++++ 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 backend/tests/controllers/teacher-invitations.test.ts diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index 3292b7bf..15bfc936 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -8,7 +8,7 @@ export async function getAllInvitationsHandler(req: Request, res: Response): Pro const by = req.query.by === 'true'; requireFields({ username }); - const invitations = getAllInvitations(username, by); + const invitations = await getAllInvitations(username, by); res.json({ invitations }); } @@ -28,11 +28,11 @@ export async function createInvitationHandler(req: Request, res: Response): Prom export async function deleteInvitationForHandler(req: Request, res: Response): Promise { const sender = req.params.sender; const receiver = req.params.receiver; - const classId = req.params.class; + const classId = req.params.classId; const accepted = req.body.accepted !== 'false'; requireFields({ sender, receiver, classId }); - const invitation = deleteInvitationFor(sender, receiver, classId, accepted); + const invitation = await deleteInvitationFor(sender, receiver, classId, accepted); res.json({ invitation }); } diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts new file mode 100644 index 00000000..8135281a --- /dev/null +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -0,0 +1,97 @@ +import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { Request, Response } from 'express'; +import { setupTestApp } from '../setup-tests.js'; +import { + createInvitationHandler, + deleteInvitationForHandler, + getAllInvitationsHandler +} from "../../src/controllers/teacher-invitations"; +import {TeacherInvitationData} from "@dwengo-1/common/interfaces/teacher-invitation"; +import {getClassHandler} from "../../src/controllers/classes"; + +describe('Teacher controllers', () => { + let req: Partial; + let res: Partial; + + let jsonMock: Mock; + + beforeAll(async () => { + await setupTestApp(); + }); + + beforeEach(() => { + jsonMock = vi.fn(); + res = { + json: jsonMock, + }; + }); + + it('Get teacher invitations by', async () => { + req = {params: {username: 'LimpBizkit'}, query: {by: 'true' }}; + + await getAllInvitationsHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({invitations: expect.anything()})); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.invitations).to.have.length.greaterThan(0); + }); + + it('Get teacher invitations for', async () => { + req = {params: {username: 'FooFighters'}, query: {by: 'false' }}; + + await getAllInvitationsHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({invitations: expect.anything()})); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.invitations).to.have.length.greaterThan(0); + }); + + it('Create and delete invitation', async () => { + const body = { + sender: 'LimpBizkit', receiver: 'testleerkracht1', + class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + } as TeacherInvitationData; + req = { body }; + + await createInvitationHandler(req as Request, res as Response); + + req = { + params: { + sender: 'LimpBizkit', receiver: 'testleerkracht1', + classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + }, body: { accepted: 'false' } + }; + + await deleteInvitationForHandler(req as Request, res as Response); + }); + + it('Create and accept invitation', async () => { + const body = { + sender: 'LimpBizkit', receiver: 'testleerkracht1', + class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + } as TeacherInvitationData; + req = { body }; + + await createInvitationHandler(req as Request, res as Response); + + req = { + params: { + sender: 'LimpBizkit', receiver: 'testleerkracht1', + classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + }, body: { accepted: 'true' } + }; + + await deleteInvitationForHandler(req as Request, res as Response); + + req = {params: { + id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + }}; + + await getClassHandler(req as Request, res as Response); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.class.teachers).toContain('testleerkracht1'); + }); +}); From 834e991568fbcdc50eb5f5f138d433d644ca6309 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sun, 13 Apr 2025 19:40:20 +0200 Subject: [PATCH 03/13] fix: invitation geeft enkel classId field terug --- backend/src/interfaces/teacher-invitation.ts | 4 ++-- backend/tests/setup-tests.ts | 8 ++++++-- common/src/interfaces/teacher-invitation.ts | 2 +- frontend/src/views/classes/TeacherClasses.vue | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index 98189938..bc2fd4af 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -10,7 +10,7 @@ export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): Teache return { sender: mapToUserDTO(invitation.sender), receiver: mapToUserDTO(invitation.receiver), - class: mapToClassDTO(invitation.class), + classId: invitation.class.classId!, }; } @@ -18,7 +18,7 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea return { sender: invitation.sender.username, receiver: invitation.receiver.username, - class: invitation.class.classId!, + classId: invitation.class.classId!, }; } diff --git a/backend/tests/setup-tests.ts b/backend/tests/setup-tests.ts index 5bd2fbd6..b666fbd2 100644 --- a/backend/tests/setup-tests.ts +++ b/backend/tests/setup-tests.ts @@ -13,6 +13,7 @@ import { makeTestAttachments } from './test_assets/content/attachments.testdata. import { makeTestQuestions } from './test_assets/questions/questions.testdata.js'; import { makeTestAnswers } from './test_assets/questions/answers.testdata.js'; import { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js'; +import {Collection} from "@mikro-orm/core"; export async function setupTestApp(): Promise { dotenv.config({ path: '.env.test' }); @@ -28,8 +29,8 @@ export async function setupTestApp(): Promise { 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); + assignments[0].groups = new Collection(groups.slice(0, 3)); + assignments[1].groups = new Collection(groups.slice(3, 4)); const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes); const classJoinRequests = makeTestClassJoinRequests(em, students, classes); @@ -41,6 +42,9 @@ export async function setupTestApp(): Promise { const answers = makeTestAnswers(em, teachers, questions); const submissions = makeTestSubmissions(em, students, groups); + console.log("classes", classes); + console.log("invitations", teacherInvitations); + await em.persistAndFlush([ ...students, ...teachers, diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index c61f9a6a..c34f46f7 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -4,7 +4,7 @@ import { ClassDTO } from './class'; export interface TeacherInvitationDTO { sender: string | UserDTO; receiver: string | UserDTO; - class: string | ClassDTO; + classId: string; } export interface TeacherInvitationData { diff --git a/frontend/src/views/classes/TeacherClasses.vue b/frontend/src/views/classes/TeacherClasses.vue index ae673d99..010cf2e2 100644 --- a/frontend/src/views/classes/TeacherClasses.vue +++ b/frontend/src/views/classes/TeacherClasses.vue @@ -276,10 +276,10 @@ - {{ (i.class as ClassDTO).displayName }} + {{ i.classId }} {{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }} From 17dc9649c5b4c4048586dd361cabc2049fd83118 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sun, 13 Apr 2025 20:50:15 +0200 Subject: [PATCH 04/13] feat: controller + queries --- frontend/src/controllers/classes.ts | 9 +-- .../src/controllers/teacher-invitations.ts | 28 ++++++++ frontend/src/queries/teacher-invitations.ts | 65 +++++++++++++++++++ 3 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 frontend/src/controllers/teacher-invitations.ts create mode 100644 frontend/src/queries/teacher-invitations.ts diff --git a/frontend/src/controllers/classes.ts b/frontend/src/controllers/classes.ts index 03e3f560..65a992a5 100644 --- a/frontend/src/controllers/classes.ts +++ b/frontend/src/controllers/classes.ts @@ -4,6 +4,7 @@ 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"; +import type {TeacherInvitationsResponse} from "@/controllers/teacher-invitations.ts"; export interface ClassesResponse { classes: ClassDTO[] | string[]; @@ -13,14 +14,6 @@ export interface ClassResponse { class: ClassDTO; } -export interface TeacherInvitationsResponse { - invites: TeacherInvitationDTO[]; -} - -export interface TeacherInvitationResponse { - invite: TeacherInvitationDTO; -} - export class ClassController extends BaseController { constructor() { super("class"); diff --git a/frontend/src/controllers/teacher-invitations.ts b/frontend/src/controllers/teacher-invitations.ts new file mode 100644 index 00000000..30201821 --- /dev/null +++ b/frontend/src/controllers/teacher-invitations.ts @@ -0,0 +1,28 @@ +import {BaseController} from "@/controllers/base-controller.ts"; +import type {TeacherInvitationData, TeacherInvitationDTO} from "@dwengo-1/common/interfaces/teacher-invitation"; + +export interface TeacherInvitationsResponse { + invitations: TeacherInvitationDTO[] +} + +export interface TeacherInvitationResponse { + invitation: TeacherInvitationDTO +} + +export class TeacherInvitationController extends BaseController { + constructor() { + super("teachers/invitations"); + } + + async getAll(username: string, by: boolean): Promise { + return this.get(`/${username}`, { by }); + } + + async create(data: TeacherInvitationData): Promise { + return this.post("/", data); + } + + async respond(data: TeacherInvitationData, accepted: boolean): Promise { + return this.delete(`/${data.sender}/${data.receiver}/${data.class}`, { accepted }); + } +} diff --git a/frontend/src/queries/teacher-invitations.ts b/frontend/src/queries/teacher-invitations.ts new file mode 100644 index 00000000..e0b1e957 --- /dev/null +++ b/frontend/src/queries/teacher-invitations.ts @@ -0,0 +1,65 @@ +import { + useMutation, + useQuery, + type UseMutationReturnType, + type UseQueryReturnType, +} from "@tanstack/vue-query"; +import { computed, toValue } from "vue"; +import type { MaybeRefOrGetter } from "vue"; +import { + TeacherInvitationController, + type TeacherInvitationResponse, + type TeacherInvitationsResponse +} from "@/controllers/teacher-invitations.ts"; +import type {TeacherInvitationData} from "@dwengo-1/common/dist/interfaces/teacher-invitation.ts"; +import type {TeacherDTO} from "@dwengo-1/common/dist/interfaces/teacher.ts"; + +const controller = new TeacherInvitationController(); + +/** + all the invitations the teacher send +**/ +export function useTeacherInvitationsByQuery(username: MaybeRefOrGetter +): UseQueryReturnType { + return useQuery({ + queryFn: computed(() => controller.getAll(toValue(username), true)), + enabled: () => Boolean(toValue(username)), + }) +} + +/** + all the pending invitations send to this teacher + */ +export function useTeacherInvitationsForQuery(username: MaybeRefOrGetter +): UseQueryReturnType { + return useQuery({ + queryFn: computed(() => controller.getAll(toValue(username), false)), + enabled: () => Boolean(toValue(username)), + }) +} + +export function useCreateTeacherInvitationMutation(): UseMutationReturnType{ + return useMutation({ + mutationFn: async (data: TeacherInvitationData) => controller.create(data) + }) +} + +export function useAcceptTeacherInvitationMutation(): UseMutationReturnType { + return useMutation({ + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, true) + }) +} + +export function useDeclineTeacherInvitationMutation(): UseMutationReturnType { + return useMutation({ + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false) + }) +} + +export function useDeleteTeacherInvitationMutation(): UseMutationReturnType { + return useMutation({ + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false) + }) +} + + From a824cdff9475d9fd4444d428080bdcd500f98f72 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sun, 13 Apr 2025 20:54:05 +0200 Subject: [PATCH 05/13] fix: lint --- backend/src/interfaces/teacher-invitation.ts | 1 - backend/src/services/teacher-invitations.ts | 2 +- backend/tests/setup-tests.ts | 3 --- common/src/interfaces/teacher-invitation.ts | 1 - frontend/src/controllers/classes.ts | 1 - frontend/src/queries/teacher-invitations.ts | 8 ++++---- 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index bc2fd4af..87741e3d 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -1,5 +1,4 @@ import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; -import { mapToClassDTO } from './class.js'; import { mapToUserDTO } from './user.js'; import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; import {getTeacherInvitationRepository} from "../data/repositories"; diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts index 1b9ef179..08886eb2 100644 --- a/backend/src/services/teacher-invitations.ts +++ b/backend/src/services/teacher-invitations.ts @@ -50,7 +50,7 @@ async function fetchInvitation(sender: Teacher, receiver: Teacher, cls: Class): return invite; } -export async function deleteInvitationFor(usernameSender: string, usernameReceiver: string, classId: string, accepted: boolean) { +export async function deleteInvitationFor(usernameSender: string, usernameReceiver: string, classId: string, accepted: boolean): Promise { const teacherInvitationRepository = getTeacherInvitationRepository(); const sender = await fetchTeacher(usernameSender); const receiver = await fetchTeacher(usernameReceiver); diff --git a/backend/tests/setup-tests.ts b/backend/tests/setup-tests.ts index b666fbd2..b0e5ef5b 100644 --- a/backend/tests/setup-tests.ts +++ b/backend/tests/setup-tests.ts @@ -42,9 +42,6 @@ export async function setupTestApp(): Promise { const answers = makeTestAnswers(em, teachers, questions); const submissions = makeTestSubmissions(em, students, groups); - console.log("classes", classes); - console.log("invitations", teacherInvitations); - await em.persistAndFlush([ ...students, ...teachers, diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index c34f46f7..b952b051 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -1,5 +1,4 @@ import { UserDTO } from './user'; -import { ClassDTO } from './class'; export interface TeacherInvitationDTO { sender: string | UserDTO; diff --git a/frontend/src/controllers/classes.ts b/frontend/src/controllers/classes.ts index 65a992a5..9020e166 100644 --- a/frontend/src/controllers/classes.ts +++ b/frontend/src/controllers/classes.ts @@ -2,7 +2,6 @@ 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"; import type {TeacherInvitationsResponse} from "@/controllers/teacher-invitations.ts"; diff --git a/frontend/src/queries/teacher-invitations.ts b/frontend/src/queries/teacher-invitations.ts index e0b1e957..4d9c5a52 100644 --- a/frontend/src/queries/teacher-invitations.ts +++ b/frontend/src/queries/teacher-invitations.ts @@ -17,23 +17,23 @@ import type {TeacherDTO} from "@dwengo-1/common/dist/interfaces/teacher.ts"; const controller = new TeacherInvitationController(); /** - all the invitations the teacher send + All the invitations the teacher send **/ export function useTeacherInvitationsByQuery(username: MaybeRefOrGetter ): UseQueryReturnType { return useQuery({ - queryFn: computed(() => controller.getAll(toValue(username), true)), + queryFn: computed(async () => controller.getAll(toValue(username), true)), enabled: () => Boolean(toValue(username)), }) } /** - all the pending invitations send to this teacher + All the pending invitations send to this teacher */ export function useTeacherInvitationsForQuery(username: MaybeRefOrGetter ): UseQueryReturnType { return useQuery({ - queryFn: computed(() => controller.getAll(toValue(username), false)), + queryFn: computed(async () => controller.getAll(toValue(username), false)), enabled: () => Boolean(toValue(username)), }) } From 783c91b2e37e6cf9ad21a23ba82343811ee552e1 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Sun, 13 Apr 2025 18:59:56 +0000 Subject: [PATCH 06/13] style: fix linting issues met Prettier --- .../src/controllers/teacher-invitations.ts | 6 +- .../classes/teacher-invitation-repository.ts | 2 +- backend/src/interfaces/teacher-invitation.ts | 10 +-- backend/src/routes/teacher-invitations.ts | 9 +-- backend/src/services/teacher-invitations.ts | 42 +++++------ .../controllers/teacher-invitations.test.ts | 52 +++++++------- backend/tests/setup-tests.ts | 2 +- frontend/src/controllers/classes.ts | 2 +- .../src/controllers/teacher-invitations.ts | 8 +-- frontend/src/queries/teacher-invitations.ts | 69 +++++++++++-------- frontend/src/views/classes/TeacherClasses.vue | 3 +- 11 files changed, 112 insertions(+), 93 deletions(-) diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index 15bfc936..b826dee3 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; -import {requireFields} from "./error-helper"; -import {createInvitation, deleteInvitationFor, getAllInvitations} from "../services/teacher-invitations"; -import {TeacherInvitationData} from "@dwengo-1/common/interfaces/teacher-invitation"; +import { requireFields } from './error-helper'; +import { createInvitation, deleteInvitationFor, getAllInvitations } from '../services/teacher-invitations'; +import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; export async function getAllInvitationsHandler(req: Request, res: Response): Promise { const username = req.params.username; diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index 5461d29b..69c0a972 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -25,6 +25,6 @@ export class TeacherInvitationRepository extends DwengoEntityRepository { const teacher = await fetchTeacher(username); @@ -29,12 +29,12 @@ export async function createInvitation(data: TeacherInvitationData): Promise { +export async function deleteInvitationFor( + usernameSender: string, + usernameReceiver: string, + classId: string, + accepted: boolean +): Promise { const teacherInvitationRepository = getTeacherInvitationRepository(); const sender = await fetchTeacher(usernameSender); const receiver = await fetchTeacher(usernameReceiver); @@ -60,12 +65,9 @@ export async function deleteInvitationFor(usernameSender: string, usernameReceiv const invitation = await fetchInvitation(sender, receiver, cls); await teacherInvitationRepository.deleteBy(cls, sender, receiver); - if (accepted){ + if (accepted) { await addClassTeacher(classId, usernameReceiver); } return mapToTeacherInvitationDTO(invitation); } - - - diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index 8135281a..6f233898 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -1,13 +1,9 @@ import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { Request, Response } from 'express'; import { setupTestApp } from '../setup-tests.js'; -import { - createInvitationHandler, - deleteInvitationForHandler, - getAllInvitationsHandler -} from "../../src/controllers/teacher-invitations"; -import {TeacherInvitationData} from "@dwengo-1/common/interfaces/teacher-invitation"; -import {getClassHandler} from "../../src/controllers/classes"; +import { createInvitationHandler, deleteInvitationForHandler, getAllInvitationsHandler } from '../../src/controllers/teacher-invitations'; +import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; +import { getClassHandler } from '../../src/controllers/classes'; describe('Teacher controllers', () => { let req: Partial; @@ -27,22 +23,22 @@ describe('Teacher controllers', () => { }); it('Get teacher invitations by', async () => { - req = {params: {username: 'LimpBizkit'}, query: {by: 'true' }}; + req = { params: { username: 'LimpBizkit' }, query: { by: 'true' } }; await getAllInvitationsHandler(req as Request, res as Response); - expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({invitations: expect.anything()})); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; expect(result.invitations).to.have.length.greaterThan(0); }); it('Get teacher invitations for', async () => { - req = {params: {username: 'FooFighters'}, query: {by: 'false' }}; + req = { params: { username: 'FooFighters' }, query: { by: 'false' } }; await getAllInvitationsHandler(req as Request, res as Response); - expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({invitations: expect.anything()})); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; expect(result.invitations).to.have.length.greaterThan(0); @@ -50,8 +46,9 @@ describe('Teacher controllers', () => { it('Create and delete invitation', async () => { const body = { - sender: 'LimpBizkit', receiver: 'testleerkracht1', - class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + sender: 'LimpBizkit', + receiver: 'testleerkracht1', + class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', } as TeacherInvitationData; req = { body }; @@ -59,9 +56,11 @@ describe('Teacher controllers', () => { req = { params: { - sender: 'LimpBizkit', receiver: 'testleerkracht1', - classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' - }, body: { accepted: 'false' } + sender: 'LimpBizkit', + receiver: 'testleerkracht1', + classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', + }, + body: { accepted: 'false' }, }; await deleteInvitationForHandler(req as Request, res as Response); @@ -69,8 +68,9 @@ describe('Teacher controllers', () => { it('Create and accept invitation', async () => { const body = { - sender: 'LimpBizkit', receiver: 'testleerkracht1', - class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' + sender: 'LimpBizkit', + receiver: 'testleerkracht1', + class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', } as TeacherInvitationData; req = { body }; @@ -78,16 +78,20 @@ describe('Teacher controllers', () => { req = { params: { - sender: 'LimpBizkit', receiver: 'testleerkracht1', - classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' - }, body: { accepted: 'true' } + sender: 'LimpBizkit', + receiver: 'testleerkracht1', + classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', + }, + body: { accepted: 'true' }, }; await deleteInvitationForHandler(req as Request, res as Response); - req = {params: { - id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' - }}; + req = { + params: { + id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', + }, + }; await getClassHandler(req as Request, res as Response); diff --git a/backend/tests/setup-tests.ts b/backend/tests/setup-tests.ts index b0e5ef5b..699e081b 100644 --- a/backend/tests/setup-tests.ts +++ b/backend/tests/setup-tests.ts @@ -13,7 +13,7 @@ import { makeTestAttachments } from './test_assets/content/attachments.testdata. import { makeTestQuestions } from './test_assets/questions/questions.testdata.js'; import { makeTestAnswers } from './test_assets/questions/answers.testdata.js'; import { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js'; -import {Collection} from "@mikro-orm/core"; +import { Collection } from '@mikro-orm/core'; export async function setupTestApp(): Promise { dotenv.config({ path: '.env.test' }); diff --git a/frontend/src/controllers/classes.ts b/frontend/src/controllers/classes.ts index 9020e166..c9b7f6fa 100644 --- a/frontend/src/controllers/classes.ts +++ b/frontend/src/controllers/classes.ts @@ -3,7 +3,7 @@ import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; import type { StudentsResponse } from "./students"; import type { AssignmentsResponse } from "./assignments"; import type { TeachersResponse } from "@/controllers/teachers.ts"; -import type {TeacherInvitationsResponse} from "@/controllers/teacher-invitations.ts"; +import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations.ts"; export interface ClassesResponse { classes: ClassDTO[] | string[]; diff --git a/frontend/src/controllers/teacher-invitations.ts b/frontend/src/controllers/teacher-invitations.ts index 30201821..a6e2045a 100644 --- a/frontend/src/controllers/teacher-invitations.ts +++ b/frontend/src/controllers/teacher-invitations.ts @@ -1,12 +1,12 @@ -import {BaseController} from "@/controllers/base-controller.ts"; -import type {TeacherInvitationData, TeacherInvitationDTO} from "@dwengo-1/common/interfaces/teacher-invitation"; +import { BaseController } from "@/controllers/base-controller.ts"; +import type { TeacherInvitationData, TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation"; export interface TeacherInvitationsResponse { - invitations: TeacherInvitationDTO[] + invitations: TeacherInvitationDTO[]; } export interface TeacherInvitationResponse { - invitation: TeacherInvitationDTO + invitation: TeacherInvitationDTO; } export class TeacherInvitationController extends BaseController { diff --git a/frontend/src/queries/teacher-invitations.ts b/frontend/src/queries/teacher-invitations.ts index 4d9c5a52..a56ea9cd 100644 --- a/frontend/src/queries/teacher-invitations.ts +++ b/frontend/src/queries/teacher-invitations.ts @@ -1,65 +1,80 @@ -import { - useMutation, - useQuery, - type UseMutationReturnType, - type UseQueryReturnType, -} from "@tanstack/vue-query"; +import { useMutation, useQuery, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { computed, toValue } from "vue"; import type { MaybeRefOrGetter } from "vue"; import { TeacherInvitationController, type TeacherInvitationResponse, - type TeacherInvitationsResponse + type TeacherInvitationsResponse, } from "@/controllers/teacher-invitations.ts"; -import type {TeacherInvitationData} from "@dwengo-1/common/dist/interfaces/teacher-invitation.ts"; -import type {TeacherDTO} from "@dwengo-1/common/dist/interfaces/teacher.ts"; +import type { TeacherInvitationData } from "@dwengo-1/common/dist/interfaces/teacher-invitation.ts"; +import type { TeacherDTO } from "@dwengo-1/common/dist/interfaces/teacher.ts"; const controller = new TeacherInvitationController(); /** All the invitations the teacher send **/ -export function useTeacherInvitationsByQuery(username: MaybeRefOrGetter +export function useTeacherInvitationsByQuery( + username: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ queryFn: computed(async () => controller.getAll(toValue(username), true)), enabled: () => Boolean(toValue(username)), - }) + }); } /** All the pending invitations send to this teacher */ -export function useTeacherInvitationsForQuery(username: MaybeRefOrGetter +export function useTeacherInvitationsForQuery( + username: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ queryFn: computed(async () => controller.getAll(toValue(username), false)), enabled: () => Boolean(toValue(username)), - }) + }); } -export function useCreateTeacherInvitationMutation(): UseMutationReturnType{ +export function useCreateTeacherInvitationMutation(): UseMutationReturnType< + TeacherInvitationResponse, + Error, + TeacherDTO, + unknown +> { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.create(data) - }) + mutationFn: async (data: TeacherInvitationData) => controller.create(data), + }); } -export function useAcceptTeacherInvitationMutation(): UseMutationReturnType { +export function useAcceptTeacherInvitationMutation(): UseMutationReturnType< + TeacherInvitationResponse, + Error, + TeacherDTO, + unknown +> { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, true) - }) + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, true), + }); } -export function useDeclineTeacherInvitationMutation(): UseMutationReturnType { +export function useDeclineTeacherInvitationMutation(): UseMutationReturnType< + TeacherInvitationResponse, + Error, + TeacherDTO, + unknown +> { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false) - }) + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false), + }); } -export function useDeleteTeacherInvitationMutation(): UseMutationReturnType { +export function useDeleteTeacherInvitationMutation(): UseMutationReturnType< + TeacherInvitationResponse, + Error, + TeacherDTO, + unknown +> { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false) - }) + mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false), + }); } - - diff --git a/frontend/src/views/classes/TeacherClasses.vue b/frontend/src/views/classes/TeacherClasses.vue index 010cf2e2..a3fba26e 100644 --- a/frontend/src/views/classes/TeacherClasses.vue +++ b/frontend/src/views/classes/TeacherClasses.vue @@ -279,7 +279,8 @@ :key="i.classId" > - {{ i.classId }} + {{ i.classId }} + {{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }} From f3d2b3313c43f30e75b52f020600bf9d2e6fe2b3 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Mon, 14 Apr 2025 16:50:54 +0200 Subject: [PATCH 07/13] feat: status teacher invite --- .../src/controllers/teacher-invitations.ts | 54 +++++++++++--- .../classes/class-join-request-repository.ts | 4 +- .../classes/teacher-invitation-repository.ts | 11 +-- .../classes/class-join-request.entity.ts | 6 +- .../classes/teacher-invitation.entity.ts | 6 +- backend/src/interfaces/student-request.ts | 4 +- backend/src/interfaces/teacher-invitation.ts | 16 +++-- backend/src/routes/teacher-invitations.ts | 13 +++- backend/src/services/teacher-invitations.ts | 70 +++++++++++-------- backend/src/services/teachers.ts | 6 +- .../controllers/teacher-invitations.test.ts | 41 +++++++++-- .../classes/class-join-requests.testdata.ts | 10 +-- .../classes/teacher-invitations.testdata.ts | 13 ++-- common/src/interfaces/class-join-request.ts | 4 +- common/src/interfaces/teacher-invitation.ts | 3 + common/src/util/class-join-request.ts | 2 +- 16 files changed, 184 insertions(+), 79 deletions(-) diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index b826dee3..f7cfa05a 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -1,11 +1,17 @@ -import { Request, Response } from 'express'; -import { requireFields } from './error-helper'; -import { createInvitation, deleteInvitationFor, getAllInvitations } from '../services/teacher-invitations'; -import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; +import {Request, Response} from 'express'; +import {requireFields} from './error-helper'; +import { + createInvitation, + deleteInvitation, + getAllInvitations, + getInvitation, + updateInvitation +} from '../services/teacher-invitations'; +import {TeacherInvitationData} from '@dwengo-1/common/interfaces/teacher-invitation'; export async function getAllInvitationsHandler(req: Request, res: Response): Promise { const username = req.params.username; - const by = req.query.by === 'true'; + const by = req.query.sent === 'true'; requireFields({ username }); const invitations = await getAllInvitations(username, by); @@ -13,6 +19,17 @@ export async function getAllInvitationsHandler(req: Request, res: Response): Pro res.json({ invitations }); } +export async function getInvitationHandler(req: Request, res: Response): Promise { + const sender = req.params.sender; + const receiver = req.params.receiver; + const classId = req.params.classId; + requireFields({ sender, receiver, classId }); + + const invitation = await getInvitation(sender, receiver, classId); + + res.json({ invitation }); +} + export async function createInvitationHandler(req: Request, res: Response): Promise { const sender = req.body.sender; const receiver = req.body.receiver; @@ -25,14 +42,29 @@ export async function createInvitationHandler(req: Request, res: Response): Prom res.json({ invitation }); } -export async function deleteInvitationForHandler(req: Request, res: Response): Promise { - const sender = req.params.sender; - const receiver = req.params.receiver; - const classId = req.params.classId; - const accepted = req.body.accepted !== 'false'; +export async function updateInvitationHandler(req: Request, res: Response): Promise { + const sender = req.body.sender; + const receiver = req.body.receiver; + const classId = req.body.class; + req.body.accepted = req.body.accepted !== 'false'; requireFields({ sender, receiver, classId }); - const invitation = await deleteInvitationFor(sender, receiver, classId, accepted); + const data = req.body as TeacherInvitationData; + const invitation = await updateInvitation(data); + + res.json({ invitation }); +} + +export async function deleteInvitationHandler(req: Request, res: Response): Promise { + const sender = req.params.sender; + const receiver = req.params.receiver; + const classId = req.params.classId; + requireFields({ sender, receiver, classId }); + + const data: TeacherInvitationData = { + sender, receiver, class: classId + }; + const invitation = await deleteInvitation(data); res.json({ invitation }); } diff --git a/backend/src/data/classes/class-join-request-repository.ts b/backend/src/data/classes/class-join-request-repository.ts index 0d9ab6e1..8bd0f81e 100644 --- a/backend/src/data/classes/class-join-request-repository.ts +++ b/backend/src/data/classes/class-join-request-repository.ts @@ -2,14 +2,14 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Class } from '../../entities/classes/class.entity.js'; import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js'; import { Student } from '../../entities/users/student.entity.js'; -import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export class ClassJoinRequestRepository extends DwengoEntityRepository { public async findAllRequestsBy(requester: Student): Promise { return this.findAll({ where: { requester: requester } }); } public async findAllOpenRequestsTo(clazz: Class): Promise { - return this.findAll({ where: { class: clazz, status: ClassJoinRequestStatus.Open } }); // TODO check if works like this + return this.findAll({ where: { class: clazz, status: ClassStatus.Open } }); // TODO check if works like this } public async findByStudentAndClass(requester: Student, clazz: Class): Promise { return this.findOne({ requester, class: clazz }); diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index 69c0a972..46abc685 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -1,7 +1,8 @@ -import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; -import { Class } from '../../entities/classes/class.entity.js'; -import { TeacherInvitation } from '../../entities/classes/teacher-invitation.entity.js'; -import { Teacher } from '../../entities/users/teacher.entity.js'; +import {DwengoEntityRepository} from '../dwengo-entity-repository.js'; +import {Class} from '../../entities/classes/class.entity.js'; +import {TeacherInvitation} from '../../entities/classes/teacher-invitation.entity.js'; +import {Teacher} from '../../entities/users/teacher.entity.js'; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; export class TeacherInvitationRepository extends DwengoEntityRepository { public async findAllInvitationsForClass(clazz: Class): Promise { @@ -11,7 +12,7 @@ export class TeacherInvitationRepository extends DwengoEntityRepository { - return this.findAll({ where: { receiver: receiver } }); + return this.findAll({ where: { receiver: receiver, status: ClassStatus.Open } }); } public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise { return this.deleteWhere({ diff --git a/backend/src/entities/classes/class-join-request.entity.ts b/backend/src/entities/classes/class-join-request.entity.ts index 907c0199..548968a6 100644 --- a/backend/src/entities/classes/class-join-request.entity.ts +++ b/backend/src/entities/classes/class-join-request.entity.ts @@ -2,7 +2,7 @@ import { Entity, Enum, ManyToOne } from '@mikro-orm/core'; import { Student } from '../users/student.entity.js'; import { Class } from './class.entity.js'; import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; -import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; @Entity({ repository: () => ClassJoinRequestRepository, @@ -20,6 +20,6 @@ export class ClassJoinRequest { }) class!: Class; - @Enum(() => ClassJoinRequestStatus) - status!: ClassJoinRequestStatus; + @Enum(() => ClassStatus) + status!: ClassStatus; } diff --git a/backend/src/entities/classes/teacher-invitation.entity.ts b/backend/src/entities/classes/teacher-invitation.entity.ts index 668a0a1c..b7451682 100644 --- a/backend/src/entities/classes/teacher-invitation.entity.ts +++ b/backend/src/entities/classes/teacher-invitation.entity.ts @@ -1,7 +1,8 @@ -import { Entity, ManyToOne } from '@mikro-orm/core'; +import {Entity, Enum, ManyToOne} from '@mikro-orm/core'; import { Teacher } from '../users/teacher.entity.js'; import { Class } from './class.entity.js'; import { TeacherInvitationRepository } from '../../data/classes/teacher-invitation-repository.js'; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; /** * Invitation of a teacher into a class (in order to teach it). @@ -25,4 +26,7 @@ export class TeacherInvitation { primary: true, }) class!: Class; + + @Enum(() => ClassStatus) + status!: ClassStatus; } diff --git a/backend/src/interfaces/student-request.ts b/backend/src/interfaces/student-request.ts index d97f5eb5..a4d3b31b 100644 --- a/backend/src/interfaces/student-request.ts +++ b/backend/src/interfaces/student-request.ts @@ -4,7 +4,7 @@ import { getClassJoinRequestRepository } from '../data/repositories.js'; import { Student } from '../entities/users/student.entity.js'; import { Class } from '../entities/classes/class.entity.js'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; -import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export function mapToStudentRequestDTO(request: ClassJoinRequest): ClassJoinRequestDTO { return { @@ -18,6 +18,6 @@ export function mapToStudentRequest(student: Student, cls: Class): ClassJoinRequ return getClassJoinRequestRepository().create({ requester: student, class: cls, - status: ClassJoinRequestStatus.Open, + status: ClassStatus.Open, }); } diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index 4d1284db..f6cd1698 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -1,15 +1,17 @@ -import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; -import { mapToUserDTO } from './user.js'; -import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; -import { getTeacherInvitationRepository } from '../data/repositories'; -import { Teacher } from '../entities/users/teacher.entity'; -import { Class } from '../entities/classes/class.entity'; +import {TeacherInvitation} from '../entities/classes/teacher-invitation.entity.js'; +import {mapToUserDTO} from './user.js'; +import {TeacherInvitationDTO} from '@dwengo-1/common/interfaces/teacher-invitation'; +import {getTeacherInvitationRepository} from '../data/repositories'; +import {Teacher} from '../entities/users/teacher.entity'; +import {Class} from '../entities/classes/class.entity'; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { return { sender: mapToUserDTO(invitation.sender), receiver: mapToUserDTO(invitation.receiver), classId: invitation.class.classId!, + status: invitation.status }; } @@ -18,6 +20,7 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea sender: invitation.sender.username, receiver: invitation.receiver.username, classId: invitation.class.classId!, + status: invitation.status }; } @@ -26,5 +29,6 @@ export function mapToInvitation(sender: Teacher, receiver: Teacher, cls: Class): sender, receiver, class: cls, + status: ClassStatus.Open, }); } diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts index 7af71fdc..6826ec21 100644 --- a/backend/src/routes/teacher-invitations.ts +++ b/backend/src/routes/teacher-invitations.ts @@ -1,12 +1,21 @@ import express from 'express'; -import { createInvitationHandler, deleteInvitationForHandler, getAllInvitationsHandler } from '../controllers/teacher-invitations'; +import { + createInvitationHandler, + deleteInvitationHandler, + getAllInvitationsHandler, getInvitationHandler, + updateInvitationHandler +} from '../controllers/teacher-invitations'; const router = express.Router({ mergeParams: true }); router.get('/:username', getAllInvitationsHandler); +router.get('/:sender/:receiver/:classId', getInvitationHandler); + router.post('/', createInvitationHandler); -router.delete('/:sender/:receiver/:classId', deleteInvitationForHandler); +router.put('/', updateInvitationHandler); + +router.delete('/:sender/:receiver/:classId', deleteInvitationHandler); export default router; diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts index dc54d5e5..19ead9a7 100644 --- a/backend/src/services/teacher-invitations.ts +++ b/backend/src/services/teacher-invitations.ts @@ -1,20 +1,19 @@ -import { fetchTeacher } from './teachers'; -import { getTeacherInvitationRepository } from '../data/repositories'; -import { mapToInvitation, mapToTeacherInvitationDTO } from '../interfaces/teacher-invitation'; -import { addClassTeacher, fetchClass } from './classes'; -import { TeacherInvitationData, TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; -import { ConflictException } from '../exceptions/conflict-exception'; -import { Teacher } from '../entities/users/teacher.entity'; -import { Class } from '../entities/classes/class.entity'; -import { NotFoundException } from '../exceptions/not-found-exception'; -import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity'; +import {fetchTeacher} from './teachers'; +import {getTeacherInvitationRepository} from '../data/repositories'; +import {mapToInvitation, mapToTeacherInvitationDTO} from '../interfaces/teacher-invitation'; +import {addClassTeacher, fetchClass} from './classes'; +import {TeacherInvitationData, TeacherInvitationDTO} from '@dwengo-1/common/interfaces/teacher-invitation'; +import {ConflictException} from '../exceptions/conflict-exception'; +import {NotFoundException} from '../exceptions/not-found-exception'; +import {TeacherInvitation} from '../entities/classes/teacher-invitation.entity'; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; -export async function getAllInvitations(username: string, by: boolean): Promise { +export async function getAllInvitations(username: string, sent: boolean): Promise { const teacher = await fetchTeacher(username); const teacherInvitationRepository = getTeacherInvitationRepository(); let invitations; - if (by) { + if (sent) { invitations = await teacherInvitationRepository.findAllInvitationsBy(teacher); } else { invitations = await teacherInvitationRepository.findAllInvitationsFor(teacher); @@ -39,7 +38,11 @@ export async function createInvitation(data: TeacherInvitationData): Promise { +async function fetchInvitation(usernameSender: string, usernameReceiver: string, classId: string): Promise { + const sender = await fetchTeacher(usernameSender); + const receiver = await fetchTeacher(usernameReceiver); + const cls = await fetchClass(classId); + const teacherInvitationRepository = getTeacherInvitationRepository(); const invite = await teacherInvitationRepository.findBy(cls, sender, receiver); @@ -50,24 +53,35 @@ async function fetchInvitation(sender: Teacher, receiver: Teacher, cls: Class): return invite; } -export async function deleteInvitationFor( - usernameSender: string, - usernameReceiver: string, - classId: string, - accepted: boolean -): Promise { - const teacherInvitationRepository = getTeacherInvitationRepository(); - const sender = await fetchTeacher(usernameSender); - const receiver = await fetchTeacher(usernameReceiver); +export async function getInvitation(sender: string, receiver: string, classId: string): Promise { + const invitation = await fetchInvitation(sender, receiver, classId); + return mapToTeacherInvitationDTO(invitation); +} - const cls = await fetchClass(classId); +export async function updateInvitation(data: TeacherInvitationData): Promise { + const invitation = await fetchInvitation(data.sender, data.receiver, data.class); + invitation.status = ClassStatus.Declined; - const invitation = await fetchInvitation(sender, receiver, cls); - await teacherInvitationRepository.deleteBy(cls, sender, receiver); - - if (accepted) { - await addClassTeacher(classId, usernameReceiver); + if (data.accepted) { + invitation.status = ClassStatus.Accepted; + await addClassTeacher(data.class, data.receiver); } + const teacherInvitationRepository = getTeacherInvitationRepository(); + await teacherInvitationRepository.save(invitation); + + return mapToTeacherInvitationDTO(invitation); +} + +export async function deleteInvitation(data: TeacherInvitationData): Promise { + const invitation = await fetchInvitation(data.sender, data.receiver, data.class); + + const sender = await fetchTeacher(data.sender); + const receiver = await fetchTeacher(data.receiver); + const cls = await fetchClass(data.class); + + const teacherInvitationRepository = getTeacherInvitationRepository(); + await teacherInvitationRepository.deleteBy(cls, sender, receiver); + return mapToTeacherInvitationDTO(invitation); } diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index e6596f9e..982b657b 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -28,7 +28,7 @@ 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 { ClassStatus } from '@dwengo-1/common/util/class-join-request'; import { ConflictException } from '../exceptions/conflict-exception.js'; export async function getAllTeachers(full: boolean): Promise { @@ -160,10 +160,10 @@ export async function updateClassJoinRequestStatus(studentUsername: string, clas throw new NotFoundException('Join request not found'); } - request.status = ClassJoinRequestStatus.Declined; + request.status = ClassStatus.Declined; if (accepted) { - request.status = ClassJoinRequestStatus.Accepted; + request.status = ClassStatus.Accepted; await addClassStudent(classId, studentUsername); } diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index 6f233898..005dac26 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -1,9 +1,15 @@ import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { Request, Response } from 'express'; import { setupTestApp } from '../setup-tests.js'; -import { createInvitationHandler, deleteInvitationForHandler, getAllInvitationsHandler } from '../../src/controllers/teacher-invitations'; +import { + createInvitationHandler, + deleteInvitationHandler, + getAllInvitationsHandler, + getInvitationHandler +} from '../../src/controllers/teacher-invitations'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; import { getClassHandler } from '../../src/controllers/classes'; +import {BadRequestException} from "../../src/exceptions/bad-request-exception"; describe('Teacher controllers', () => { let req: Partial; @@ -23,13 +29,14 @@ describe('Teacher controllers', () => { }); it('Get teacher invitations by', async () => { - req = { params: { username: 'LimpBizkit' }, query: { by: 'true' } }; + req = { params: { username: 'LimpBizkit' }, query: { sent: 'true' } }; await getAllInvitationsHandler(req as Request, res as Response); expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; + console.log(result.invitations); expect(result.invitations).to.have.length.greaterThan(0); }); @@ -63,9 +70,33 @@ describe('Teacher controllers', () => { body: { accepted: 'false' }, }; - await deleteInvitationForHandler(req as Request, res as Response); + await deleteInvitationHandler(req as Request, res as Response); }); + it('Get invitation', async () => { + req = { + params: { + sender: 'LimpBizkit', + receiver: 'FooFighters', + classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', + }, + }; + await getInvitationHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitation: expect.anything() })); + }); + + it('Get invitation error', async () => { + req = { + params: { no: 'no params' }, + }; + + await expect( async () => getInvitationHandler(req as Request, res as Response)) + .rejects.toThrowError(BadRequestException); + }); + + /* + it('Create and accept invitation', async () => { const body = { sender: 'LimpBizkit', @@ -85,7 +116,7 @@ describe('Teacher controllers', () => { body: { accepted: 'true' }, }; - await deleteInvitationForHandler(req as Request, res as Response); + await deleteInvitationHandler(req as Request, res as Response); req = { params: { @@ -98,4 +129,6 @@ describe('Teacher controllers', () => { const result = jsonMock.mock.lastCall?.[0]; expect(result.class.teachers).toContain('testleerkracht1'); }); + + */ }); diff --git a/backend/tests/test_assets/classes/class-join-requests.testdata.ts b/backend/tests/test_assets/classes/class-join-requests.testdata.ts index 32337b19..63319dc4 100644 --- a/backend/tests/test_assets/classes/class-join-requests.testdata.ts +++ b/backend/tests/test_assets/classes/class-join-requests.testdata.ts @@ -2,31 +2,31 @@ import { EntityManager } from '@mikro-orm/core'; import { ClassJoinRequest } from '../../../src/entities/classes/class-join-request.entity'; import { Student } from '../../../src/entities/users/student.entity'; import { Class } from '../../../src/entities/classes/class.entity'; -import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export function makeTestClassJoinRequests(em: EntityManager, students: Student[], classes: Class[]): ClassJoinRequest[] { const classJoinRequest01 = em.create(ClassJoinRequest, { requester: students[4], class: classes[1], - status: ClassJoinRequestStatus.Open, + status: ClassStatus.Open, }); const classJoinRequest02 = em.create(ClassJoinRequest, { requester: students[2], class: classes[1], - status: ClassJoinRequestStatus.Open, + status: ClassStatus.Open, }); const classJoinRequest03 = em.create(ClassJoinRequest, { requester: students[4], class: classes[2], - status: ClassJoinRequestStatus.Open, + status: ClassStatus.Open, }); const classJoinRequest04 = em.create(ClassJoinRequest, { requester: students[3], class: classes[2], - status: ClassJoinRequestStatus.Open, + status: ClassStatus.Open, }); return [classJoinRequest01, classJoinRequest02, classJoinRequest03, classJoinRequest04]; diff --git a/backend/tests/test_assets/classes/teacher-invitations.testdata.ts b/backend/tests/test_assets/classes/teacher-invitations.testdata.ts index 68204a57..35a3d1b8 100644 --- a/backend/tests/test_assets/classes/teacher-invitations.testdata.ts +++ b/backend/tests/test_assets/classes/teacher-invitations.testdata.ts @@ -1,31 +1,36 @@ -import { EntityManager } from '@mikro-orm/core'; -import { TeacherInvitation } from '../../../src/entities/classes/teacher-invitation.entity'; -import { Teacher } from '../../../src/entities/users/teacher.entity'; -import { Class } from '../../../src/entities/classes/class.entity'; +import {EntityManager} from '@mikro-orm/core'; +import {TeacherInvitation} from '../../../src/entities/classes/teacher-invitation.entity'; +import {Teacher} from '../../../src/entities/users/teacher.entity'; +import {Class} from '../../../src/entities/classes/class.entity'; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; export function makeTestTeacherInvitations(em: EntityManager, teachers: Teacher[], classes: Class[]): TeacherInvitation[] { const teacherInvitation01 = em.create(TeacherInvitation, { sender: teachers[1], receiver: teachers[0], class: classes[1], + status: ClassStatus.Open }); const teacherInvitation02 = em.create(TeacherInvitation, { sender: teachers[1], receiver: teachers[2], class: classes[1], + status: ClassStatus.Open }); const teacherInvitation03 = em.create(TeacherInvitation, { sender: teachers[2], receiver: teachers[0], class: classes[2], + status: ClassStatus.Open }); const teacherInvitation04 = em.create(TeacherInvitation, { sender: teachers[0], receiver: teachers[1], class: classes[0], + status: ClassStatus.Open }); return [teacherInvitation01, teacherInvitation02, teacherInvitation03, teacherInvitation04]; diff --git a/common/src/interfaces/class-join-request.ts b/common/src/interfaces/class-join-request.ts index 6787998b..5e8b2683 100644 --- a/common/src/interfaces/class-join-request.ts +++ b/common/src/interfaces/class-join-request.ts @@ -1,8 +1,8 @@ import { StudentDTO } from './student'; -import { ClassJoinRequestStatus } from '../util/class-join-request'; +import { ClassStatus } from '../util/class-join-request'; export interface ClassJoinRequestDTO { requester: StudentDTO; class: string; - status: ClassJoinRequestStatus; + status: ClassStatus; } diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index b952b051..37a4029f 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -1,13 +1,16 @@ import { UserDTO } from './user'; +import {ClassStatus} from "../util/class-join-request"; export interface TeacherInvitationDTO { sender: string | UserDTO; receiver: string | UserDTO; classId: string; + status: ClassStatus; } export interface TeacherInvitationData { sender: string; receiver: string; class: string; + accepted?: boolean; } diff --git a/common/src/util/class-join-request.ts b/common/src/util/class-join-request.ts index 5f9410f0..2049a16d 100644 --- a/common/src/util/class-join-request.ts +++ b/common/src/util/class-join-request.ts @@ -1,4 +1,4 @@ -export enum ClassJoinRequestStatus { +export enum ClassStatus { Open = 'open', Accepted = 'accepted', Declined = 'declined', From 8e2643f596033375339986b2021bc1c6fcfe5473 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Mon, 14 Apr 2025 14:52:59 +0000 Subject: [PATCH 08/13] style: fix linting issues met ESLint --- backend/tests/controllers/teacher-invitations.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index 005dac26..c7f61773 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -97,7 +97,7 @@ describe('Teacher controllers', () => { /* - it('Create and accept invitation', async () => { + It('Create and accept invitation', async () => { const body = { sender: 'LimpBizkit', receiver: 'testleerkracht1', From d88add83511c6d5a5ed9f06d9bd957a9dd4f20b4 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Mon, 14 Apr 2025 14:53:04 +0000 Subject: [PATCH 09/13] style: fix linting issues met Prettier --- backend/src/controllers/teacher-invitations.ts | 18 +++++++----------- .../classes/teacher-invitation-repository.ts | 10 +++++----- .../classes/teacher-invitation.entity.ts | 4 ++-- backend/src/interfaces/teacher-invitation.ts | 18 +++++++++--------- backend/src/routes/teacher-invitations.ts | 5 +++-- backend/src/services/teacher-invitations.ts | 18 +++++++++--------- .../controllers/teacher-invitations.test.ts | 7 +++---- .../classes/teacher-invitations.testdata.ts | 18 +++++++++--------- common/src/interfaces/teacher-invitation.ts | 2 +- 9 files changed, 48 insertions(+), 52 deletions(-) diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index f7cfa05a..4956f3e2 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -1,13 +1,7 @@ -import {Request, Response} from 'express'; -import {requireFields} from './error-helper'; -import { - createInvitation, - deleteInvitation, - getAllInvitations, - getInvitation, - updateInvitation -} from '../services/teacher-invitations'; -import {TeacherInvitationData} from '@dwengo-1/common/interfaces/teacher-invitation'; +import { Request, Response } from 'express'; +import { requireFields } from './error-helper'; +import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations'; +import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; export async function getAllInvitationsHandler(req: Request, res: Response): Promise { const username = req.params.username; @@ -62,7 +56,9 @@ export async function deleteInvitationHandler(req: Request, res: Response): Prom requireFields({ sender, receiver, classId }); const data: TeacherInvitationData = { - sender, receiver, class: classId + sender, + receiver, + class: classId, }; const invitation = await deleteInvitation(data); diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index 46abc685..c9442e29 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -1,8 +1,8 @@ -import {DwengoEntityRepository} from '../dwengo-entity-repository.js'; -import {Class} from '../../entities/classes/class.entity.js'; -import {TeacherInvitation} from '../../entities/classes/teacher-invitation.entity.js'; -import {Teacher} from '../../entities/users/teacher.entity.js'; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; +import { Class } from '../../entities/classes/class.entity.js'; +import { TeacherInvitation } from '../../entities/classes/teacher-invitation.entity.js'; +import { Teacher } from '../../entities/users/teacher.entity.js'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export class TeacherInvitationRepository extends DwengoEntityRepository { public async findAllInvitationsForClass(clazz: Class): Promise { diff --git a/backend/src/entities/classes/teacher-invitation.entity.ts b/backend/src/entities/classes/teacher-invitation.entity.ts index b7451682..6059f155 100644 --- a/backend/src/entities/classes/teacher-invitation.entity.ts +++ b/backend/src/entities/classes/teacher-invitation.entity.ts @@ -1,8 +1,8 @@ -import {Entity, Enum, ManyToOne} from '@mikro-orm/core'; +import { Entity, Enum, ManyToOne } from '@mikro-orm/core'; import { Teacher } from '../users/teacher.entity.js'; import { Class } from './class.entity.js'; import { TeacherInvitationRepository } from '../../data/classes/teacher-invitation-repository.js'; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; /** * Invitation of a teacher into a class (in order to teach it). diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index f6cd1698..8fef17af 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -1,17 +1,17 @@ -import {TeacherInvitation} from '../entities/classes/teacher-invitation.entity.js'; -import {mapToUserDTO} from './user.js'; -import {TeacherInvitationDTO} from '@dwengo-1/common/interfaces/teacher-invitation'; -import {getTeacherInvitationRepository} from '../data/repositories'; -import {Teacher} from '../entities/users/teacher.entity'; -import {Class} from '../entities/classes/class.entity'; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; +import { mapToUserDTO } from './user.js'; +import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; +import { getTeacherInvitationRepository } from '../data/repositories'; +import { Teacher } from '../entities/users/teacher.entity'; +import { Class } from '../entities/classes/class.entity'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { return { sender: mapToUserDTO(invitation.sender), receiver: mapToUserDTO(invitation.receiver), classId: invitation.class.classId!, - status: invitation.status + status: invitation.status, }; } @@ -20,7 +20,7 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea sender: invitation.sender.username, receiver: invitation.receiver.username, classId: invitation.class.classId!, - status: invitation.status + status: invitation.status, }; } diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts index 6826ec21..772b1351 100644 --- a/backend/src/routes/teacher-invitations.ts +++ b/backend/src/routes/teacher-invitations.ts @@ -2,8 +2,9 @@ import express from 'express'; import { createInvitationHandler, deleteInvitationHandler, - getAllInvitationsHandler, getInvitationHandler, - updateInvitationHandler + getAllInvitationsHandler, + getInvitationHandler, + updateInvitationHandler, } from '../controllers/teacher-invitations'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts index 19ead9a7..07f61bae 100644 --- a/backend/src/services/teacher-invitations.ts +++ b/backend/src/services/teacher-invitations.ts @@ -1,12 +1,12 @@ -import {fetchTeacher} from './teachers'; -import {getTeacherInvitationRepository} from '../data/repositories'; -import {mapToInvitation, mapToTeacherInvitationDTO} from '../interfaces/teacher-invitation'; -import {addClassTeacher, fetchClass} from './classes'; -import {TeacherInvitationData, TeacherInvitationDTO} from '@dwengo-1/common/interfaces/teacher-invitation'; -import {ConflictException} from '../exceptions/conflict-exception'; -import {NotFoundException} from '../exceptions/not-found-exception'; -import {TeacherInvitation} from '../entities/classes/teacher-invitation.entity'; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { fetchTeacher } from './teachers'; +import { getTeacherInvitationRepository } from '../data/repositories'; +import { mapToInvitation, mapToTeacherInvitationDTO } from '../interfaces/teacher-invitation'; +import { addClassTeacher, fetchClass } from './classes'; +import { TeacherInvitationData, TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; +import { ConflictException } from '../exceptions/conflict-exception'; +import { NotFoundException } from '../exceptions/not-found-exception'; +import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export async function getAllInvitations(username: string, sent: boolean): Promise { const teacher = await fetchTeacher(username); diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index c7f61773..38184d7d 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -5,11 +5,11 @@ import { createInvitationHandler, deleteInvitationHandler, getAllInvitationsHandler, - getInvitationHandler + getInvitationHandler, } from '../../src/controllers/teacher-invitations'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; import { getClassHandler } from '../../src/controllers/classes'; -import {BadRequestException} from "../../src/exceptions/bad-request-exception"; +import { BadRequestException } from '../../src/exceptions/bad-request-exception'; describe('Teacher controllers', () => { let req: Partial; @@ -91,8 +91,7 @@ describe('Teacher controllers', () => { params: { no: 'no params' }, }; - await expect( async () => getInvitationHandler(req as Request, res as Response)) - .rejects.toThrowError(BadRequestException); + await expect(async () => getInvitationHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); }); /* diff --git a/backend/tests/test_assets/classes/teacher-invitations.testdata.ts b/backend/tests/test_assets/classes/teacher-invitations.testdata.ts index 35a3d1b8..6337a513 100644 --- a/backend/tests/test_assets/classes/teacher-invitations.testdata.ts +++ b/backend/tests/test_assets/classes/teacher-invitations.testdata.ts @@ -1,36 +1,36 @@ -import {EntityManager} from '@mikro-orm/core'; -import {TeacherInvitation} from '../../../src/entities/classes/teacher-invitation.entity'; -import {Teacher} from '../../../src/entities/users/teacher.entity'; -import {Class} from '../../../src/entities/classes/class.entity'; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { EntityManager } from '@mikro-orm/core'; +import { TeacherInvitation } from '../../../src/entities/classes/teacher-invitation.entity'; +import { Teacher } from '../../../src/entities/users/teacher.entity'; +import { Class } from '../../../src/entities/classes/class.entity'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; export function makeTestTeacherInvitations(em: EntityManager, teachers: Teacher[], classes: Class[]): TeacherInvitation[] { const teacherInvitation01 = em.create(TeacherInvitation, { sender: teachers[1], receiver: teachers[0], class: classes[1], - status: ClassStatus.Open + status: ClassStatus.Open, }); const teacherInvitation02 = em.create(TeacherInvitation, { sender: teachers[1], receiver: teachers[2], class: classes[1], - status: ClassStatus.Open + status: ClassStatus.Open, }); const teacherInvitation03 = em.create(TeacherInvitation, { sender: teachers[2], receiver: teachers[0], class: classes[2], - status: ClassStatus.Open + status: ClassStatus.Open, }); const teacherInvitation04 = em.create(TeacherInvitation, { sender: teachers[0], receiver: teachers[1], class: classes[0], - status: ClassStatus.Open + status: ClassStatus.Open, }); return [teacherInvitation01, teacherInvitation02, teacherInvitation03, teacherInvitation04]; diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index 37a4029f..0979bceb 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -1,5 +1,5 @@ import { UserDTO } from './user'; -import {ClassStatus} from "../util/class-join-request"; +import { ClassStatus } from '../util/class-join-request'; export interface TeacherInvitationDTO { sender: string | UserDTO; From ef04f6c7af638bb29e5e7f04d2c8cabf8cdf17d1 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Tue, 15 Apr 2025 17:33:03 +0200 Subject: [PATCH 10/13] fix: put route frontend + veranderingen delete --- .../controllers/teacher-invitations.test.ts | 26 +++++-------- common/src/interfaces/teacher-invitation.ts | 2 +- .../src/controllers/teacher-invitations.ts | 16 ++++++-- frontend/src/queries/teacher-invitations.ts | 38 +++++++++---------- 4 files changed, 40 insertions(+), 42 deletions(-) diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index 005dac26..c9e0d623 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -5,11 +5,12 @@ import { createInvitationHandler, deleteInvitationHandler, getAllInvitationsHandler, - getInvitationHandler + getInvitationHandler, updateInvitationHandler } from '../../src/controllers/teacher-invitations'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; import { getClassHandler } from '../../src/controllers/classes'; import {BadRequestException} from "../../src/exceptions/bad-request-exception"; +import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; describe('Teacher controllers', () => { let req: Partial; @@ -95,28 +96,19 @@ describe('Teacher controllers', () => { .rejects.toThrowError(BadRequestException); }); - /* - it('Create and accept invitation', async () => { + it('Accept invitation', async () => { const body = { sender: 'LimpBizkit', - receiver: 'testleerkracht1', + receiver: 'FooFighters', class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', } as TeacherInvitationData; req = { body }; - await createInvitationHandler(req as Request, res as Response); + await updateInvitationHandler(req as Request, res as Response); - req = { - params: { - sender: 'LimpBizkit', - receiver: 'testleerkracht1', - classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', - }, - body: { accepted: 'true' }, - }; - - await deleteInvitationHandler(req as Request, res as Response); + const result1 = jsonMock.mock.lastCall?.[0]; + expect(result1.invitation.status).toEqual(ClassStatus.Accepted); req = { params: { @@ -127,8 +119,8 @@ describe('Teacher controllers', () => { await getClassHandler(req as Request, res as Response); const result = jsonMock.mock.lastCall?.[0]; - expect(result.class.teachers).toContain('testleerkracht1'); + expect(result.class.teachers).toContain('FooFighters'); }); - */ + }); diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index 37a4029f..0d310d0e 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -12,5 +12,5 @@ export interface TeacherInvitationData { sender: string; receiver: string; class: string; - accepted?: boolean; + accepted?: boolean; // use for put requests, else skip } diff --git a/frontend/src/controllers/teacher-invitations.ts b/frontend/src/controllers/teacher-invitations.ts index a6e2045a..8083c152 100644 --- a/frontend/src/controllers/teacher-invitations.ts +++ b/frontend/src/controllers/teacher-invitations.ts @@ -14,15 +14,23 @@ export class TeacherInvitationController extends BaseController { super("teachers/invitations"); } - async getAll(username: string, by: boolean): Promise { - return this.get(`/${username}`, { by }); + async getAll(username: string, sent: boolean): Promise { + return this.get(`/${username}`, { sent }); + } + + async getBy(data: TeacherInvitationData): Promise { + return this.get(`/${data.sender}/${data.receiver}/${data.class}`) } async create(data: TeacherInvitationData): Promise { return this.post("/", data); } - async respond(data: TeacherInvitationData, accepted: boolean): Promise { - return this.delete(`/${data.sender}/${data.receiver}/${data.class}`, { accepted }); + async remove(data: TeacherInvitationData): Promise { + return this.delete(`/${data.sender}/${data.receiver}/${data.class}`); + } + + async respond(data: TeacherInvitationData) { + return this.put("/", data); } } diff --git a/frontend/src/queries/teacher-invitations.ts b/frontend/src/queries/teacher-invitations.ts index a56ea9cd..59357c32 100644 --- a/frontend/src/queries/teacher-invitations.ts +++ b/frontend/src/queries/teacher-invitations.ts @@ -6,15 +6,15 @@ import { type TeacherInvitationResponse, type TeacherInvitationsResponse, } from "@/controllers/teacher-invitations.ts"; -import type { TeacherInvitationData } from "@dwengo-1/common/dist/interfaces/teacher-invitation.ts"; -import type { TeacherDTO } from "@dwengo-1/common/dist/interfaces/teacher.ts"; +import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation"; +import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; const controller = new TeacherInvitationController(); /** - All the invitations the teacher send + All the invitations the teacher sent **/ -export function useTeacherInvitationsByQuery( +export function useTeacherInvitationsSentQuery( username: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ @@ -24,9 +24,9 @@ export function useTeacherInvitationsByQuery( } /** - All the pending invitations send to this teacher + All the pending invitations sent to this teacher */ -export function useTeacherInvitationsForQuery( +export function useTeacherInvitationsReceivedQuery( username: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ @@ -35,6 +35,15 @@ export function useTeacherInvitationsForQuery( }); } +export function useTeacherInvitationQuery( + data: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryFn: computed(async () => controller.getBy(toValue(data))), + enabled: () => Boolean(toValue(data)), + }); +} + export function useCreateTeacherInvitationMutation(): UseMutationReturnType< TeacherInvitationResponse, Error, @@ -46,25 +55,14 @@ export function useCreateTeacherInvitationMutation(): UseMutationReturnType< }); } -export function useAcceptTeacherInvitationMutation(): UseMutationReturnType< +export function useRespondTeacherInvitationMutation(): UseMutationReturnType< TeacherInvitationResponse, Error, TeacherDTO, unknown > { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, true), - }); -} - -export function useDeclineTeacherInvitationMutation(): UseMutationReturnType< - TeacherInvitationResponse, - Error, - TeacherDTO, - unknown -> { - return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false), + mutationFn: async (data: TeacherInvitationData) => controller.respond(data), }); } @@ -75,6 +73,6 @@ export function useDeleteTeacherInvitationMutation(): UseMutationReturnType< unknown > { return useMutation({ - mutationFn: async (data: TeacherInvitationData) => controller.respond(data, false), + mutationFn: async (data: TeacherInvitationData) => controller.remove(data), }); } From d84e6b485e76dbb38c8ad67223026b591434f1b7 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Tue, 15 Apr 2025 17:36:13 +0200 Subject: [PATCH 11/13] fix: lint --- backend/tests/controllers/teacher-invitations.test.ts | 2 +- common/src/interfaces/teacher-invitation.ts | 2 +- frontend/src/controllers/teacher-invitations.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index c9e0d623..f14d3b3a 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -37,7 +37,7 @@ describe('Teacher controllers', () => { expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; - console.log(result.invitations); + // console.log(result.invitations); expect(result.invitations).to.have.length.greaterThan(0); }); diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts index 0d310d0e..3158b806 100644 --- a/common/src/interfaces/teacher-invitation.ts +++ b/common/src/interfaces/teacher-invitation.ts @@ -12,5 +12,5 @@ export interface TeacherInvitationData { sender: string; receiver: string; class: string; - accepted?: boolean; // use for put requests, else skip + accepted?: boolean; // Use for put requests, else skip } diff --git a/frontend/src/controllers/teacher-invitations.ts b/frontend/src/controllers/teacher-invitations.ts index 8083c152..f3383395 100644 --- a/frontend/src/controllers/teacher-invitations.ts +++ b/frontend/src/controllers/teacher-invitations.ts @@ -30,7 +30,7 @@ export class TeacherInvitationController extends BaseController { return this.delete(`/${data.sender}/${data.receiver}/${data.class}`); } - async respond(data: TeacherInvitationData) { + async respond(data: TeacherInvitationData): Promise { return this.put("/", data); } } From 447aeee9f3b504ec1bbea2b29226cc9394293908 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Tue, 15 Apr 2025 15:44:17 +0000 Subject: [PATCH 12/13] style: fix linting issues met ESLint --- backend/tests/controllers/teacher-invitations.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index f14d3b3a..2e9b594f 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -37,7 +37,7 @@ describe('Teacher controllers', () => { expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; - // console.log(result.invitations); + // Console.log(result.invitations); expect(result.invitations).to.have.length.greaterThan(0); }); From 63045c4223571cb84615111be9672c46f413277a Mon Sep 17 00:00:00 2001 From: Lint Action Date: Tue, 15 Apr 2025 15:44:22 +0000 Subject: [PATCH 13/13] style: fix linting issues met Prettier --- .../tests/controllers/teacher-invitations.test.ts | 13 +++++-------- frontend/src/controllers/teacher-invitations.ts | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/tests/controllers/teacher-invitations.test.ts b/backend/tests/controllers/teacher-invitations.test.ts index 2e9b594f..ed2f5ebf 100644 --- a/backend/tests/controllers/teacher-invitations.test.ts +++ b/backend/tests/controllers/teacher-invitations.test.ts @@ -5,12 +5,13 @@ import { createInvitationHandler, deleteInvitationHandler, getAllInvitationsHandler, - getInvitationHandler, updateInvitationHandler + getInvitationHandler, + updateInvitationHandler, } from '../../src/controllers/teacher-invitations'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; import { getClassHandler } from '../../src/controllers/classes'; -import {BadRequestException} from "../../src/exceptions/bad-request-exception"; -import {ClassStatus} from "@dwengo-1/common/util/class-join-request"; +import { BadRequestException } from '../../src/exceptions/bad-request-exception'; +import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; describe('Teacher controllers', () => { let req: Partial; @@ -92,11 +93,9 @@ describe('Teacher controllers', () => { params: { no: 'no params' }, }; - await expect( async () => getInvitationHandler(req as Request, res as Response)) - .rejects.toThrowError(BadRequestException); + await expect(async () => getInvitationHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); }); - it('Accept invitation', async () => { const body = { sender: 'LimpBizkit', @@ -121,6 +120,4 @@ describe('Teacher controllers', () => { const result = jsonMock.mock.lastCall?.[0]; expect(result.class.teachers).toContain('FooFighters'); }); - - }); diff --git a/frontend/src/controllers/teacher-invitations.ts b/frontend/src/controllers/teacher-invitations.ts index f3383395..7750dea5 100644 --- a/frontend/src/controllers/teacher-invitations.ts +++ b/frontend/src/controllers/teacher-invitations.ts @@ -19,7 +19,7 @@ export class TeacherInvitationController extends BaseController { } async getBy(data: TeacherInvitationData): Promise { - return this.get(`/${data.sender}/${data.receiver}/${data.class}`) + return this.get(`/${data.sender}/${data.receiver}/${data.class}`); } async create(data: TeacherInvitationData): Promise {