diff --git a/backend/src/data/dwengo-entity-repository.ts b/backend/src/data/dwengo-entity-repository.ts index f17b6976..64a129ce 100644 --- a/backend/src/data/dwengo-entity-repository.ts +++ b/backend/src/data/dwengo-entity-repository.ts @@ -1,12 +1,25 @@ -import { EntityRepository, FilterQuery } from '@mikro-orm/core'; +import { EntityRepository, FilterQuery, SyntaxErrorException } from '@mikro-orm/core'; import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js'; +import { getLogger } from '../logging/initalize.js'; export abstract class DwengoEntityRepository extends EntityRepository { public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise { if (options?.preventOverwrite && (await this.findOne(entity))) { throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`); } - await this.getEntityManager().persistAndFlush(entity); + try { + await this.getEntityManager().persistAndFlush(entity); + } catch (e: unknown) { + // Workaround for MikroORM bug: Sometimes, queries are generated with random syntax errors. + // The faulty query is then retried everytime something is persisted. By clearing the entity + // Manager in that case, we make sure that future queries will work. + if (e instanceof SyntaxErrorException) { + getLogger().error('SyntaxErrorException caught => entity manager cleared.'); + this.em.clear(); + } else { + throw e; + } + } } public async deleteWhere(query: FilterQuery): Promise { const toDelete = await this.findOne(query); diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 6a3c0ead..342751f2 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -18,13 +18,8 @@ export class QuestionRepository extends DwengoEntityRepository { content: question.content, timestamp: new Date(), }); - await this.insert(questionEntity); - questionEntity.learningObjectHruid = question.loId.hruid; - questionEntity.learningObjectLanguage = question.loId.language; - questionEntity.learningObjectVersion = question.loId.version; - questionEntity.author = question.author; - questionEntity.inGroup = question.inGroup; - questionEntity.content = question.content; + // Don't check for overwrite since this is impossible anyway due to autoincrement. + await this.save(questionEntity, { preventOverwrite: false }); return questionEntity; } public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise { diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 44ccfbd3..0b41f2f7 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -6,6 +6,9 @@ import { Group } from '../assignments/group.entity.js'; @Entity({ repository: () => QuestionRepository }) export class Question { + @PrimaryKey({ type: 'integer', autoincrement: true }) + sequenceNumber?: number; + @PrimaryKey({ type: 'string' }) learningObjectHruid!: string; @@ -18,9 +21,6 @@ export class Question { @PrimaryKey({ type: 'number' }) learningObjectVersion = 1; - @PrimaryKey({ type: 'integer', autoincrement: true }) - sequenceNumber?: number; - @ManyToOne({ entity: () => Group }) inGroup!: Group; diff --git a/backend/src/middleware/auth/checks/submission-checks.ts b/backend/src/middleware/auth/checks/submission-checks.ts index 9caae176..1b781773 100644 --- a/backend/src/middleware/auth/checks/submission-checks.ts +++ b/backend/src/middleware/auth/checks/submission-checks.ts @@ -34,15 +34,15 @@ export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: Authentic }); export const onlyAllowIfHasAccessToSubmissionFromParams = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { - const { classId, assignmentId, groupId } = req.query; + const { classId, assignmentId, forGroup } = req.query; - requireFields({ classId, assignmentId, groupId }); + requireFields({ classId, assignmentId, forGroup }); if (auth.accountType === AccountType.Teacher) { const cls = await fetchClass(classId as string); return cls.teachers.map(mapToUsername).includes(auth.username); } - const group = await fetchGroup(classId as string, Number(assignmentId as string), Number(groupId as string)); + const group = await fetchGroup(classId as string, Number(assignmentId as string), Number(forGroup as string)); return group.members.map(mapToUsername).includes(auth.username); }); diff --git a/backend/src/services/assignments.ts b/backend/src/services/assignments.ts index b9ad9212..afa4c759 100644 --- a/backend/src/services/assignments.ts +++ b/backend/src/services/assignments.ts @@ -110,17 +110,26 @@ export async function putAssignment(classid: string, id: number, assignmentData: const studentLists = await Promise.all((assignmentData.groups as string[][]).map(async (group) => await fetchStudents(group))); - const groupRepository = getGroupRepository(); - await groupRepository.deleteAllByAssignment(assignment); - await Promise.all( - studentLists.map(async (students) => { - const newGroup = groupRepository.create({ - assignment: assignment, - members: students, - }); - await groupRepository.save(newGroup); - }) - ); + try { + const groupRepository = getGroupRepository(); + await groupRepository.deleteAllByAssignment(assignment); + + await Promise.all( + studentLists.map(async (students) => { + const newGroup = groupRepository.create({ + assignment: assignment, + members: students, + }); + await groupRepository.save(newGroup); + }) + ); + } catch (e: unknown) { + if (e instanceof ForeignKeyConstraintViolationException || e instanceof PostgreSqlExceptionConverter) { + throw new ConflictException('Cannot update assigment with questions or submissions'); + } else { + throw e; + } + } delete assignmentData.groups; } diff --git a/frontend/src/components/GroupSubmissionStatus.vue b/frontend/src/components/GroupSubmissionStatus.vue index d8559cab..2cb4cb7a 100644 --- a/frontend/src/components/GroupSubmissionStatus.vue +++ b/frontend/src/components/GroupSubmissionStatus.vue @@ -1,11 +1,13 @@ @@ -401,13 +408,15 @@ diff --git a/frontend/src/views/assignments/UserAssignments.vue b/frontend/src/views/assignments/UserAssignments.vue index 0fbdb6d2..09e84c15 100644 --- a/frontend/src/views/assignments/UserAssignments.vue +++ b/frontend/src/views/assignments/UserAssignments.vue @@ -1,5 +1,5 @@ @@ -40,7 +36,8 @@ :label="t('viewAsGroup')" :items="groupOptions(data.groups)" v-model="model" - item-title="label" + :item-title="(item) => labelForGroup(data.groups, parseInt(`${item}`))" + :item-value="(item) => item" class="group-selector-cb" variant="outlined" clearable diff --git a/frontend/src/views/learning-paths/LearningPathPage.vue b/frontend/src/views/learning-paths/LearningPathPage.vue index a7ac9a51..90cfab7c 100644 --- a/frontend/src/views/learning-paths/LearningPathPage.vue +++ b/frontend/src/views/learning-paths/LearningPathPage.vue @@ -13,7 +13,7 @@ import authService from "@/services/auth/auth-service.ts"; import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; import LearningPathGroupSelector from "@/views/learning-paths/LearningPathGroupSelector.vue"; - import { useQuestionsQuery } from "@/queries/questions"; + import { useQuestionsGroupQuery, useQuestionsQuery } from "@/queries/questions"; import type { QuestionsResponse } from "@/controllers/questions"; import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; import QandA from "@/components/QandA.vue"; @@ -56,7 +56,12 @@ const learningObjectListQueryResult = useLearningObjectListForPathQuery(learningPathQueryResult.data); const nodesList: ComputedRef = computed( - () => learningPathQueryResult.data.value?.nodesAsList ?? null, + () => + learningPathQueryResult.data.value?.nodesAsList.filter( + (node) => + authService.authState.activeRole === AccountType.Teacher || + !getLearningObjectForNode(node)?.teacherExclusive, + ) ?? null, ); const currentNode = computed(() => { @@ -76,19 +81,44 @@ return currentIndex < nodesList.value?.length ? nodesList.value?.[currentIndex - 1] : undefined; }); - const getQuestionsQuery = useQuestionsQuery( - computed( - () => - ({ - language: currentNode.value?.language, - hruid: currentNode.value?.learningobjectHruid, - version: currentNode.value?.version, - }) as LearningObjectIdentifierDTO, - ), - ); + let getQuestionsQuery; + + if (authService.authState.activeRole === AccountType.Student) { + getQuestionsQuery = useQuestionsGroupQuery( + computed( + () => + ({ + language: currentNode.value?.language, + hruid: currentNode.value?.learningobjectHruid, + version: currentNode.value?.version, + }) as LearningObjectIdentifierDTO, + ), + computed(() => query.value.classId ?? ""), + computed(() => query.value.assignmentNo ?? ""), + computed(() => authService.authState.user?.profile.preferred_username ?? ""), + ); + } else { + getQuestionsQuery = useQuestionsQuery( + computed( + () => + ({ + language: currentNode.value?.language, + hruid: currentNode.value?.learningobjectHruid, + version: currentNode.value?.version, + }) as LearningObjectIdentifierDTO, + ), + ); + } const navigationDrawerShown = ref(true); + function getLearningObjectForNode(node: LearningPathNode): LearningObject | undefined { + return learningObjectListQueryResult.data.value?.find( + (obj) => + obj.key === node.learningobjectHruid && obj.language === node.language && obj.version === node.version, + ); + } + function isLearningObjectCompleted(learningObject: LearningObject): boolean { if (learningObjectListQueryResult.isSuccess) { return ( @@ -155,6 +185,20 @@ "/" + currentNode.value?.learningobjectHruid, ); + + /** + * Filter the given list of questions such that only the questions for the assignment and group specified + * in the query parameters are shown. This is relevant for teachers since they can view questions of all groups. + */ + function filterQuestions(questions?: QuestionDTO[]): QuestionDTO[] { + return ( + questions?.filter( + (q) => + q.inGroup.groupNumber === forGroup.value?.forGroup && + q.inGroup.assignment === forGroup.value?.assignmentNo, + ) ?? [] + ); + }