diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts index 2ecb35cb..928d4cce 100644 --- a/backend/src/controllers/assignments.ts +++ b/backend/src/controllers/assignments.ts @@ -66,7 +66,7 @@ export async function putAssignmentHandler(req: Request, res: Response): Promise res.json({ assignment }); } -export async function deleteAssignmentHandler(req: Request, _res: Response): Promise { +export async function deleteAssignmentHandler(req: Request, res: Response): Promise { const id = Number(req.params.id); const classid = req.params.classid; requireFields({ id, classid }); @@ -75,7 +75,8 @@ export async function deleteAssignmentHandler(req: Request, _res: Response): Pro throw new BadRequestException('Assignment id should be a number'); } - await deleteAssignment(classid, id); + const assignment = await deleteAssignment(classid, id); + res.json({ assignment }); } export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise { diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts index 53bc96ec..217510f6 100644 --- a/backend/src/controllers/groups.ts +++ b/backend/src/controllers/groups.ts @@ -3,8 +3,6 @@ import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupSubmissions, 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'; function checkGroupFields(classId: string, assignmentId: number, groupId: number): void { requireFields({ classId, assignmentId, groupId }); @@ -35,7 +33,11 @@ export async function putGroupHandler(req: Request, res: Response): Promise>); + // Only members field can be changed + const members = req.body.members; + requireFields({ members }); + + const group = await putGroup(classId, assignmentId, groupId, { members } as Partial); res.json({ group }); } diff --git a/backend/src/entities/assignments/assignment.entity.ts b/backend/src/entities/assignments/assignment.entity.ts index ed8745f6..a12ffbac 100644 --- a/backend/src/entities/assignments/assignment.entity.ts +++ b/backend/src/entities/assignments/assignment.entity.ts @@ -1,4 +1,4 @@ -import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; +import { Cascade, Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Class } from '../classes/class.entity.js'; import { Group } from './group.entity.js'; import { Language } from '@dwengo-1/common/util/language'; @@ -34,6 +34,7 @@ export class Assignment { @OneToMany({ entity: () => Group, mappedBy: 'assignment', + cascade: [Cascade.ALL], }) groups: Collection = new Collection(this); } diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index 4018c3af..b19a99eb 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -1,6 +1,6 @@ import { Student } from '../users/student.entity.js'; import { Group } from './group.entity.js'; -import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; +import { Entity, Enum, ManyToOne, PrimaryKey, Property, Cascade } from '@mikro-orm/core'; import { SubmissionRepository } from '../../data/assignments/submission-repository.js'; import { Language } from '@dwengo-1/common/util/language'; @@ -21,8 +21,8 @@ export class Submission { @PrimaryKey({ type: 'numeric', autoincrement: false }) learningObjectVersion = 1; - @ManyToOne({ - entity: () => Group, + @ManyToOne(() => Group, { + cascade: [Cascade.REMOVE], }) onBehalfOf!: Group; diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 0c73c8c5..382780d8 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -7,8 +7,17 @@ import { GroupDTO, GroupDTOId } from '@dwengo-1/common/interfaces/group'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { fetchAssignment } from './assignments.js'; import { NotFoundException } from '../exceptions/not-found-exception.js'; -import { putObject } from './service-helper.js'; import { fetchStudents } from './students.js'; +import { fetchClass } from './classes.js'; +import { BadRequestException } from '../exceptions/bad-request-exception.js'; +import { Student } from '../entities/users/student.entity.js'; +import { Class } from '../entities/classes/class.entity.js'; + +async function assertMembersInClass(members: Student[], cls: Class): Promise { + if (!members.every((student) => cls.students.contains(student))) { + throw new BadRequestException('Student does not belong to class'); + } +} export async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise { const assignment = await fetchAssignment(classId, assignmentNumber); @@ -28,15 +37,18 @@ export async function getGroup(classId: string, assignmentNumber: number, groupN return mapToGroupDTO(group, group.assignment.within); } -export async function putGroup( - classId: string, - assignmentNumber: number, - groupNumber: number, - groupData: Partial> -): Promise { +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()); + const memberUsernames = groupData.members as string[]; + const members = await fetchStudents(memberUsernames); + + const cls = await fetchClass(classId); + await assertMembersInClass(members, cls); + + const groupRepository = getGroupRepository(); + groupRepository.assign(group, { members } as Partial>); + await groupRepository.getEntityManager().persistAndFlush(group); return mapToGroupDTO(group, group.assignment.within); } @@ -63,6 +75,9 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme const memberUsernames = (groupData.members as string[]) || []; const members = await fetchStudents(memberUsernames); + const cls = await fetchClass(classid); + await assertMembersInClass(members, cls); + const assignment = await fetchAssignment(classid, assignmentNumber); const groupRepository = getGroupRepository(); diff --git a/backend/tests/data/assignments/submissions.test.ts b/backend/tests/data/assignments/submissions.test.ts index 2bbd00dc..ea2341bc 100644 --- a/backend/tests/data/assignments/submissions.test.ts +++ b/backend/tests/data/assignments/submissions.test.ts @@ -17,6 +17,7 @@ import { ClassRepository } from '../../../src/data/classes/class-repository'; import { Submission } from '../../../src/entities/assignments/submission.entity'; import { Class } from '../../../src/entities/classes/class.entity'; import { Assignment } from '../../../src/entities/assignments/assignment.entity'; +import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata'; describe('SubmissionRepository', () => { let submissionRepository: SubmissionRepository; @@ -106,7 +107,7 @@ describe('SubmissionRepository', () => { }); it('should not find a deleted submission', async () => { - const id = new LearningObjectIdentifier('id01', Language.English, 1); + const id = new LearningObjectIdentifier(testLearningObject01.hruid, testLearningObject01.language, testLearningObject01.version); await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1); const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1); diff --git a/backend/tests/test_assets/assignments/groups.testdata.ts b/backend/tests/test_assets/assignments/groups.testdata.ts index 16674843..4361383b 100644 --- a/backend/tests/test_assets/assignments/groups.testdata.ts +++ b/backend/tests/test_assets/assignments/groups.testdata.ts @@ -61,7 +61,7 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen */ group1ConditionalLearningPath = em.create(Group, { assignment: getConditionalPathAssignment(), - groupNumber: 1, + groupNumber: 21005, members: [getTestleerling1()], }); diff --git a/frontend/package.json b/frontend/package.json index cccb6b29..d34e15f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "dependencies": { "@tanstack/react-query": "^5.69.0", "@tanstack/vue-query": "^5.69.0", + "@vueuse/core": "^13.1.0", "axios": "^1.8.2", "oidc-client-ts": "^3.1.0", "uuid": "^11.1.0", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b1207448..dbb62e79 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,6 +3,9 @@ import MenuBar from "@/components/MenuBar.vue"; import { useRoute } from "vue-router"; import { computed } from "vue"; + import authService from "@/services/auth/auth-service.ts"; + + void authService.loadUser(); const route = useRoute(); interface RouteMeta { diff --git a/frontend/src/assets/assignment.css b/frontend/src/assets/assignment.css new file mode 100644 index 00000000..029dec22 --- /dev/null +++ b/frontend/src/assets/assignment.css @@ -0,0 +1,49 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + padding: 2%; + min-height: 100vh; +} + +.assignment-card { + width: 80%; + padding: 2%; + border-radius: 12px; +} + +.description { + margin-top: 2%; + line-height: 1.6; + font-size: 1.1rem; +} + +.top-right-btn { + position: absolute; + right: 2%; + color: red; +} + +.group-section { + margin-top: 2rem; +} + +.group-section h3 { + margin-bottom: 0.5rem; +} + +.group-section ul { + padding-left: 1.2rem; + list-style-type: disc; +} + +.subtitle-section { + display: flex; + align-items: center; + justify-content: space-between; +} + +.assignmentTopTitle { + white-space: normal; + word-break: break-word; +} diff --git a/frontend/src/components/BrowseThemes.vue b/frontend/src/components/BrowseThemes.vue index 805d2720..b65c4e26 100644 --- a/frontend/src/components/BrowseThemes.vue +++ b/frontend/src/components/BrowseThemes.vue @@ -11,7 +11,7 @@ selectedAge: { type: String, required: true }, }); - const { locale } = useI18n(); + const { t, locale } = useI18n(); const language = computed(() => locale.value); const { data: allThemes, isLoading, error } = useThemeQuery(language); @@ -74,6 +74,22 @@ class="fill-height" /> + + + diff --git a/frontend/src/components/MenuBar.vue b/frontend/src/components/MenuBar.vue index 0954707f..e3734976 100644 --- a/frontend/src/components/MenuBar.vue +++ b/frontend/src/components/MenuBar.vue @@ -11,7 +11,7 @@ const { t, locale } = useI18n(); const role = auth.authState.activeRole; - const router = useRouter(); + const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable const name: string = auth.authState.user!.profile.name!; const initials: string = name diff --git a/frontend/src/components/ThemeCard.vue b/frontend/src/components/ThemeCard.vue index 7064b63c..d2420474 100644 --- a/frontend/src/components/ThemeCard.vue +++ b/frontend/src/components/ThemeCard.vue @@ -1,21 +1,26 @@