Merge pull request #289 from SELab-2/fix/questions-toon-enkel-groep
fix: Questions, submissions & discussions
This commit is contained in:
		
						commit
						d68564c953
					
				
					 18 changed files with 280 additions and 188 deletions
				
			
		|  | @ -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 { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js'; | ||||||
|  | import { getLogger } from '../logging/initalize.js'; | ||||||
| 
 | 
 | ||||||
| export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { | export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { | ||||||
|     public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> { |     public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> { | ||||||
|         if (options?.preventOverwrite && (await this.findOne(entity))) { |         if (options?.preventOverwrite && (await this.findOne(entity))) { | ||||||
|             throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`); |             throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`); | ||||||
|         } |         } | ||||||
|  |         try { | ||||||
|             await this.getEntityManager().persistAndFlush(entity); |             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<T>): Promise<void> { |     public async deleteWhere(query: FilterQuery<T>): Promise<void> { | ||||||
|         const toDelete = await this.findOne(query); |         const toDelete = await this.findOne(query); | ||||||
|  |  | ||||||
|  | @ -18,13 +18,8 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|             content: question.content, |             content: question.content, | ||||||
|             timestamp: new Date(), |             timestamp: new Date(), | ||||||
|         }); |         }); | ||||||
|         await this.insert(questionEntity); |         // Don't check for overwrite since this is impossible anyway due to autoincrement.
 | ||||||
|         questionEntity.learningObjectHruid = question.loId.hruid; |         await this.save(questionEntity, { preventOverwrite: false }); | ||||||
|         questionEntity.learningObjectLanguage = question.loId.language; |  | ||||||
|         questionEntity.learningObjectVersion = question.loId.version; |  | ||||||
|         questionEntity.author = question.author; |  | ||||||
|         questionEntity.inGroup = question.inGroup; |  | ||||||
|         questionEntity.content = question.content; |  | ||||||
|         return questionEntity; |         return questionEntity; | ||||||
|     } |     } | ||||||
|     public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { |     public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { | ||||||
|  |  | ||||||
|  | @ -6,6 +6,9 @@ import { Group } from '../assignments/group.entity.js'; | ||||||
| 
 | 
 | ||||||
| @Entity({ repository: () => QuestionRepository }) | @Entity({ repository: () => QuestionRepository }) | ||||||
| export class Question { | export class Question { | ||||||
|  |     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||||
|  |     sequenceNumber?: number; | ||||||
|  | 
 | ||||||
|     @PrimaryKey({ type: 'string' }) |     @PrimaryKey({ type: 'string' }) | ||||||
|     learningObjectHruid!: string; |     learningObjectHruid!: string; | ||||||
| 
 | 
 | ||||||
|  | @ -18,9 +21,6 @@ export class Question { | ||||||
|     @PrimaryKey({ type: 'number' }) |     @PrimaryKey({ type: 'number' }) | ||||||
|     learningObjectVersion = 1; |     learningObjectVersion = 1; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) |  | ||||||
|     sequenceNumber?: number; |  | ||||||
| 
 |  | ||||||
|     @ManyToOne({ entity: () => Group }) |     @ManyToOne({ entity: () => Group }) | ||||||
|     inGroup!: Group; |     inGroup!: Group; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -34,15 +34,15 @@ export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: Authentic | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export const onlyAllowIfHasAccessToSubmissionFromParams = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | 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) { |     if (auth.accountType === AccountType.Teacher) { | ||||||
|         const cls = await fetchClass(classId as string); |         const cls = await fetchClass(classId as string); | ||||||
|         return cls.teachers.map(mapToUsername).includes(auth.username); |         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); |     return group.members.map(mapToUsername).includes(auth.username); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -110,8 +110,10 @@ 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 studentLists = await Promise.all((assignmentData.groups as string[][]).map(async (group) => await fetchStudents(group))); | ||||||
| 
 | 
 | ||||||
|  |         try { | ||||||
|             const groupRepository = getGroupRepository(); |             const groupRepository = getGroupRepository(); | ||||||
|             await groupRepository.deleteAllByAssignment(assignment); |             await groupRepository.deleteAllByAssignment(assignment); | ||||||
|  | 
 | ||||||
|             await Promise.all( |             await Promise.all( | ||||||
|                 studentLists.map(async (students) => { |                 studentLists.map(async (students) => { | ||||||
|                     const newGroup = groupRepository.create({ |                     const newGroup = groupRepository.create({ | ||||||
|  | @ -121,6 +123,13 @@ export async function putAssignment(classid: string, id: number, assignmentData: | ||||||
|                     await groupRepository.save(newGroup); |                     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; |         delete assignmentData.groups; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,11 +1,13 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|     import { useAssignmentSubmissionsQuery } from "@/queries/assignments.ts"; |  | ||||||
|     import type { SubmissionsResponse } from "@/controllers/submissions.ts"; |     import type { SubmissionsResponse } from "@/controllers/submissions.ts"; | ||||||
|     import { watch } from "vue"; |     import { ref, watch } from "vue"; | ||||||
|  |     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||||
| 
 | 
 | ||||||
|     const props = defineProps<{ |     const props = defineProps<{ | ||||||
|  |         learningPathHruid: string; | ||||||
|  |         language: string; | ||||||
|         group: object; |         group: object; | ||||||
|         assignmentId: number; |         assignmentId: number; | ||||||
|         classId: string; |         classId: string; | ||||||
|  | @ -15,18 +17,24 @@ | ||||||
|     const emit = defineEmits<(e: "update:hasSubmission", hasSubmission: boolean) => void>(); |     const emit = defineEmits<(e: "update:hasSubmission", hasSubmission: boolean) => void>(); | ||||||
| 
 | 
 | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
|     const submissionsQuery = useAssignmentSubmissionsQuery( |     const hasMadeProgress = ref(false); | ||||||
|         () => props.classId, | 
 | ||||||
|         () => props.assignmentId, |     const getLearningPathQuery = useGetLearningPathQuery( | ||||||
|         () => props.group.originalGroupNo, |         () => props.learningPathHruid, | ||||||
|         () => true, |         () => props.language, | ||||||
|  |         () => ({ | ||||||
|  |             forGroup: props.group.originalGroupNo, | ||||||
|  |             assignmentNo: props.assignmentId, | ||||||
|  |             classId: props.classId, | ||||||
|  |         }), | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     watch( |     watch( | ||||||
|         () => submissionsQuery.data.value, |         () => getLearningPathQuery.data.value, | ||||||
|         (data) => { |         (learningPath) => { | ||||||
|             if (data) { |             if (learningPath) { | ||||||
|                 emit("update:hasSubmission", data.submissions.length > 0); |                 hasMadeProgress.value = learningPath.amountOfNodes !== learningPath.amountOfNodesLeft; | ||||||
|  |                 emit("update:hasSubmission", hasMadeProgress.value); | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         { immediate: true }, |         { immediate: true }, | ||||||
|  | @ -35,16 +43,16 @@ | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <using-query-result |     <using-query-result | ||||||
|         :query-result="submissionsQuery" |         :query-result="getLearningPathQuery" | ||||||
|         v-slot="{ data }: { data: SubmissionsResponse }" |         v-slot="{ data }: { data: SubmissionsResponse }" | ||||||
|     > |     > | ||||||
|         <v-btn |         <v-btn | ||||||
|             :color="data?.submissions?.length > 0 ? 'green' : 'red'" |             :color="hasMadeProgress ? 'green' : 'red'" | ||||||
|             variant="text" |             variant="text" | ||||||
|             :to="data.submissions.length > 0 ? goToGroupSubmissionLink(props.group.groupNo) : undefined" |             :to="hasMadeProgress ? goToGroupSubmissionLink(props.group.originalGroupNo) : undefined" | ||||||
|             :disabled="data.submissions.length === 0" |             :disabled="!hasMadeProgress" | ||||||
|         > |         > | ||||||
|             {{ data.submissions.length > 0 ? t("submission") : t("noSubmissionsYet") }} |             {{ hasMadeProgress ? t("submission") : t("noSubmissionsYet") }} | ||||||
|         </v-btn> |         </v-btn> | ||||||
|     </using-query-result> |     </using-query-result> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -1,92 +1,47 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import authService from "@/services/auth/auth-service.ts"; |     import authService from "@/services/auth/auth-service.ts"; | ||||||
|     import { Language } from "@/data-objects/language.ts"; |  | ||||||
|     import { computed, type ComputedRef, ref } from "vue"; |     import { computed, type ComputedRef, ref } from "vue"; | ||||||
|     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; |     import type { GroupDTOId } from "@dwengo-1/common/interfaces/group"; | ||||||
|     import { useStudentAssignmentsQuery, useStudentGroupsQuery } from "@/queries/students.ts"; |  | ||||||
|     import type { GroupDTO, GroupDTOId } from "@dwengo-1/common/interfaces/group"; |  | ||||||
|     import type { QuestionData } from "@dwengo-1/common/interfaces/question"; |     import type { QuestionData } from "@dwengo-1/common/interfaces/question"; | ||||||
|     import type { LearningObjectIdentifierDTO } from "@dwengo-1/interfaces/learning-content"; |     import type { LearningObjectIdentifierDTO } from "@dwengo-1/interfaces/learning-content"; | ||||||
|     import { useCreateQuestionMutation, useQuestionsQuery } from "@/queries/questions.ts"; |     import { useCreateQuestionMutation } from "@/queries/questions.ts"; | ||||||
|     import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; |  | ||||||
|     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; |  | ||||||
|     import { useLearningObjectListForPathQuery } from "@/queries/learning-objects.ts"; |  | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import { AccountType } from "@dwengo-1/common/util/account-types.ts"; |     import { AccountType } from "@dwengo-1/common/util/account-types.ts"; | ||||||
| 
 | 
 | ||||||
|     const props = defineProps<{ |     const props = defineProps<{ | ||||||
|         hruid: string; |         learningObjectHruid: string; | ||||||
|         language: Language; |         learningObjectLanguage: string; | ||||||
|         learningObjectHruid?: string; |         learningObjectVersion: number; | ||||||
|         forGroup?: GroupDTOId | undefined; |         forGroup?: GroupDTOId | undefined; | ||||||
|         withTitle?: boolean; |         withTitle?: boolean; | ||||||
|     }>(); |     }>(); | ||||||
| 
 | 
 | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|     const studentAssignmentsQueryResult = useStudentAssignmentsQuery( |     const emit = defineEmits(["updated"]); | ||||||
|         authService.authState.user?.profile.preferred_username, |  | ||||||
|     ); |  | ||||||
|     const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, props.forGroup); |  | ||||||
|     const learningObjectListQueryResult = useLearningObjectListForPathQuery(learningPathQueryResult.data); |  | ||||||
| 
 |  | ||||||
|     const pathIsAssignment = computed(() => { |  | ||||||
|         const assignments = (studentAssignmentsQueryResult.data.value?.assignments as AssignmentDTO[]) || []; |  | ||||||
|         return assignments.some( |  | ||||||
|             (assignment) => assignment.learningPath === props.hruid && assignment.language === props.language, |  | ||||||
|         ); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const nodesList: ComputedRef<LearningPathNode[] | null> = computed( |  | ||||||
|         () => learningPathQueryResult.data.value?.nodesAsList ?? null, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const currentNode = computed(() => { |  | ||||||
|         const currentHruid = props.learningObjectHruid; |  | ||||||
|         return nodesList.value?.find((it) => it.learningobjectHruid === currentHruid); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const getQuestionsQuery = useQuestionsQuery( |  | ||||||
|         computed( |  | ||||||
|             () => |  | ||||||
|                 ({ |  | ||||||
|                     language: currentNode.value?.language, |  | ||||||
|                     hruid: currentNode.value?.learningobjectHruid, |  | ||||||
|                     version: currentNode.value?.version, |  | ||||||
|                 }) as LearningObjectIdentifierDTO, |  | ||||||
|         ), |  | ||||||
|     ); |  | ||||||
| 
 | 
 | ||||||
|     const questionInput = ref(""); |     const questionInput = ref(""); | ||||||
| 
 | 
 | ||||||
|     const loID: ComputedRef<LearningObjectIdentifierDTO> = computed(() => ({ |     const loID: ComputedRef<LearningObjectIdentifierDTO> = computed(() => ({ | ||||||
|         hruid: props.learningObjectHruid as string, |         hruid: props.learningObjectHruid as string, | ||||||
|         language: props.language, |         language: props.learningObjectLanguage, | ||||||
|  |         version: props.learningObjectVersion, | ||||||
|     })); |     })); | ||||||
|     const createQuestionMutation = useCreateQuestionMutation(loID); |     const createQuestionMutation = useCreateQuestionMutation(loID); | ||||||
|     const groupsQueryResult = useStudentGroupsQuery(authService.authState.user?.profile.preferred_username); |  | ||||||
| 
 | 
 | ||||||
|     const showQuestionBox = computed( |     const showQuestionBox = computed(() => authService.authState.activeRole === AccountType.Student && props.forGroup); | ||||||
|         () => authService.authState.activeRole === AccountType.Student && pathIsAssignment.value, |  | ||||||
|     ); |  | ||||||
| 
 | 
 | ||||||
|     function submitQuestion(): void { |     function submitQuestion(): void { | ||||||
|         const assignments = studentAssignmentsQueryResult.data.value?.assignments as AssignmentDTO[]; |         if (props.forGroup && questionInput.value !== "") { | ||||||
|         const assignment = assignments.find( |  | ||||||
|             (assignment) => assignment.learningPath === props.hruid && assignment.language === props.language, |  | ||||||
|         ); |  | ||||||
|         const groups = groupsQueryResult.data.value?.groups as GroupDTO[]; |  | ||||||
|         const group = groups?.find((group) => group.assignment === assignment?.id) as GroupDTO; |  | ||||||
|             const questionData: QuestionData = { |             const questionData: QuestionData = { | ||||||
|                 author: authService.authState.user?.profile.preferred_username, |                 author: authService.authState.user?.profile.preferred_username, | ||||||
|                 content: questionInput.value, |                 content: questionInput.value, | ||||||
|             inGroup: group, |                 inGroup: props.forGroup, | ||||||
|             }; |             }; | ||||||
|         if (questionInput.value !== "") { |  | ||||||
|             createQuestionMutation.mutate(questionData, { |             createQuestionMutation.mutate(questionData, { | ||||||
|                 onSuccess: async () => { |                 onSuccess: async () => { | ||||||
|                     questionInput.value = ""; // Clear the input field after submission |                     questionInput.value = ""; // Clear the input field after submission | ||||||
|                     await getQuestionsQuery.refetch(); // Reload the questions |                     emit("updated"); | ||||||
|                 }, |                 }, | ||||||
|                 onError: (_) => { |                 onError: (_) => { | ||||||
|                     // TODO Handle error |                     // TODO Handle error | ||||||
|  |  | ||||||
|  | @ -48,4 +48,12 @@ export class AssignmentController extends BaseController { | ||||||
|     async getGroups(assignmentNumber: number, full = true): Promise<GroupsResponse> { |     async getGroups(assignmentNumber: number, full = true): Promise<GroupsResponse> { | ||||||
|         return this.get<GroupsResponse>(`/${assignmentNumber}/groups`, { full }); |         return this.get<GroupsResponse>(`/${assignmentNumber}/groups`, { full }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     async getSubmissionsByGroup( | ||||||
|  |         assignmentNumber: number, | ||||||
|  |         groupNumber: number, | ||||||
|  |         full = true, | ||||||
|  |     ): Promise<SubmissionsResponse> { | ||||||
|  |         return this.get<SubmissionsResponse>(`/${assignmentNumber}/groups/${groupNumber}/submissions`, { full }); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -18,6 +18,15 @@ export class QuestionController extends BaseController { | ||||||
|         this.loId = loId; |         this.loId = loId; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async getAllGroup( | ||||||
|  |         classId: string, | ||||||
|  |         assignmentId: string, | ||||||
|  |         forStudent: string, | ||||||
|  |         full = true, | ||||||
|  |     ): Promise<QuestionsResponse> { | ||||||
|  |         return this.get<QuestionsResponse>("/", { lang: this.loId.language, full, classId, assignmentId, forStudent }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async getAll(full = true): Promise<QuestionsResponse> { |     async getAll(full = true): Promise<QuestionsResponse> { | ||||||
|         return this.get<QuestionsResponse>("/", { lang: this.loId.language, full }); |         return this.get<QuestionsResponse>("/", { lang: this.loId.language, full }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -23,7 +23,14 @@ export class SubmissionController extends BaseController { | ||||||
|         groupId?: number, |         groupId?: number, | ||||||
|         full = true, |         full = true, | ||||||
|     ): Promise<SubmissionsResponse> { |     ): Promise<SubmissionsResponse> { | ||||||
|         return this.get<SubmissionsResponse>(`/`, { language, version, classId, assignmentId, groupId, full }); |         return this.get<SubmissionsResponse>(`/`, { | ||||||
|  |             language, | ||||||
|  |             version, | ||||||
|  |             classId, | ||||||
|  |             assignmentId, | ||||||
|  |             forGroup: groupId, | ||||||
|  |             full, | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getByNumber( |     async getByNumber( | ||||||
|  | @ -39,7 +46,7 @@ export class SubmissionController extends BaseController { | ||||||
|             version, |             version, | ||||||
|             classId, |             classId, | ||||||
|             assignmentId, |             assignmentId, | ||||||
|             groupId, |             forGroup: groupId, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -181,7 +181,7 @@ export function useAssignmentSubmissionsQuery( | ||||||
| 
 | 
 | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: computed(() => assignmentSubmissionsQueryKey(cid!, an!, f)), |         queryKey: computed(() => assignmentSubmissionsQueryKey(cid!, an!, f)), | ||||||
|         queryFn: async () => new AssignmentController(cid!).getSubmissions(an!, f), |         queryFn: async () => new AssignmentController(cid!).getSubmissionsByGroup(an!, gn!, f), | ||||||
|         enabled: () => checkEnabled(cid, an, gn), |         enabled: () => checkEnabled(cid, an, gn), | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import { | ||||||
|     useQueryClient, |     useQueryClient, | ||||||
|     type UseQueryReturnType, |     type UseQueryReturnType, | ||||||
| } from "@tanstack/vue-query"; | } from "@tanstack/vue-query"; | ||||||
|  | import type { Language } from "@dwengo-1/common/util/language"; | ||||||
| 
 | 
 | ||||||
| export function questionsQueryKey( | export function questionsQueryKey( | ||||||
|     loId: LearningObjectIdentifierDTO, |     loId: LearningObjectIdentifierDTO, | ||||||
|  | @ -17,6 +18,16 @@ export function questionsQueryKey( | ||||||
|     return ["questions", loId.hruid, loId.version!, loId.language, full]; |     return ["questions", loId.hruid, loId.version!, loId.language, full]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function questionsGroupQueryKey( | ||||||
|  |     loId: LearningObjectIdentifierDTO, | ||||||
|  |     classId: string, | ||||||
|  |     assignmentId: string, | ||||||
|  |     student: string, | ||||||
|  |     full: boolean, | ||||||
|  | ): [string, string, number, Language, boolean, string, string, string] { | ||||||
|  |     return ["questions", loId.hruid, loId.version!, loId.language, full, classId, assignmentId, student]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function questionQueryKey(questionId: QuestionId): [string, string, number, string, number] { | export function questionQueryKey(questionId: QuestionId): [string, string, number, string, number] { | ||||||
|     const loId = questionId.learningObjectIdentifier; |     const loId = questionId.learningObjectIdentifier; | ||||||
|     return ["question", loId.hruid, loId.version!, loId.language, questionId.sequenceNumber]; |     return ["question", loId.hruid, loId.version!, loId.language, questionId.sequenceNumber]; | ||||||
|  | @ -33,6 +44,34 @@ export function useQuestionsQuery( | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function useQuestionsGroupQuery( | ||||||
|  |     loId: MaybeRefOrGetter<LearningObjectIdentifierDTO>, | ||||||
|  |     classId: MaybeRefOrGetter<string>, | ||||||
|  |     assignmentId: MaybeRefOrGetter<string>, | ||||||
|  |     student: MaybeRefOrGetter<string>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = true, | ||||||
|  | ): UseQueryReturnType<QuestionsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => | ||||||
|  |             questionsGroupQueryKey( | ||||||
|  |                 toValue(loId), | ||||||
|  |                 toValue(classId), | ||||||
|  |                 toValue(assignmentId), | ||||||
|  |                 toValue(student), | ||||||
|  |                 toValue(full), | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |         queryFn: async () => | ||||||
|  |             new QuestionController(toValue(loId)).getAllGroup( | ||||||
|  |                 toValue(classId), | ||||||
|  |                 toValue(assignmentId), | ||||||
|  |                 toValue(student), | ||||||
|  |                 toValue(full), | ||||||
|  |             ), | ||||||
|  |         enabled: () => Boolean(toValue(loId)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function useQuestionQuery( | export function useQuestionQuery( | ||||||
|     questionId: MaybeRefOrGetter<QuestionId>, |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
| ): UseQueryReturnType<QuestionResponse, Error> { | ): UseQueryReturnType<QuestionResponse, Error> { | ||||||
|  |  | ||||||
|  | @ -62,7 +62,10 @@ | ||||||
|         const { valid } = await form.value.validate(); |         const { valid } = await form.value.validate(); | ||||||
|         if (!valid) return; |         if (!valid) return; | ||||||
| 
 | 
 | ||||||
|         const lp = lpIsSelected.value ? route.query.hruid?.toString() : selectedLearningPath.value?.hruid; |         const lp = lpIsSelected.value | ||||||
|  |             ? { hruid: route.query.hruid!.toString(), language: language.value } | ||||||
|  |             : { hruid: selectedLearningPath.value!.hruid, language: selectedLearningPath.value!.language }; | ||||||
|  | 
 | ||||||
|         if (!lp) { |         if (!lp) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | @ -72,8 +75,8 @@ | ||||||
|             within: selectedClass.value?.id || "", |             within: selectedClass.value?.id || "", | ||||||
|             title: assignmentTitle.value, |             title: assignmentTitle.value, | ||||||
|             description: "", |             description: "", | ||||||
|             learningPath: lp, |             learningPath: lp.hruid, | ||||||
|             language: language.value, |             language: lp.language, | ||||||
|             deadline: null, |             deadline: null, | ||||||
|             groups: [], |             groups: [], | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  | @ -1,18 +1,18 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import { ref, computed, watchEffect } from "vue"; |     import { computed, type ComputedRef, ref, watchEffect } from "vue"; | ||||||
|     import auth from "@/services/auth/auth-service.ts"; |     import auth from "@/services/auth/auth-service.ts"; | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|     import { asyncComputed } from "@vueuse/core"; |     import { asyncComputed } from "@vueuse/core"; | ||||||
|     import { |     import { useStudentsByUsernamesQuery } from "@/queries/students.ts"; | ||||||
|         useStudentAssignmentsQuery, |  | ||||||
|         useStudentGroupsQuery, |  | ||||||
|         useStudentsByUsernamesQuery, |  | ||||||
|     } from "@/queries/students.ts"; |  | ||||||
|     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; |     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||||
|     import type { Language } from "@/data-objects/language.ts"; |     import type { Language } from "@/data-objects/language.ts"; | ||||||
|     import { calculateProgress } from "@/utils/assignment-utils.ts"; |     import { calculateProgress } from "@/utils/assignment-utils.ts"; | ||||||
|     import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; |     import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||||
|  |     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||||
|  |     import { useAssignmentQuery } from "@/queries/assignments.ts"; | ||||||
|  |     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||||
|  |     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||||
| 
 | 
 | ||||||
|     const props = defineProps<{ |     const props = defineProps<{ | ||||||
|         classId: string; |         classId: string; | ||||||
|  | @ -28,32 +28,24 @@ | ||||||
|         return user?.profile?.preferred_username ?? undefined; |         return user?.profile?.preferred_username ?? undefined; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const assignmentsQueryResult = useStudentAssignmentsQuery(username, true); |     const assignmentQueryResult = useAssignmentQuery(props.classId, props.assignmentId); | ||||||
| 
 | 
 | ||||||
|     const assignment = computed(() => { |     const assignment: ComputedRef<AssignmentDTO | undefined> = computed( | ||||||
|         const assignments = assignmentsQueryResult.data.value?.assignments; |         () => assignmentQueryResult.data.value?.assignment, | ||||||
|         if (!assignments) return undefined; |     ); | ||||||
| 
 |  | ||||||
|         return assignments.find((a) => a.id === props.assignmentId && a.within === props.classId); |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     learningPath.value = assignment.value?.learningPath; |     learningPath.value = assignment.value?.learningPath; | ||||||
| 
 | 
 | ||||||
|     const groupsQueryResult = useStudentGroupsQuery(username, true); |  | ||||||
|     const group = computed(() => { |     const group = computed(() => { | ||||||
|         const groups = groupsQueryResult.data.value?.groups as GroupDTO[]; |         const groups = assignment.value?.groups as GroupDTO[]; | ||||||
| 
 | 
 | ||||||
|         if (!groups) return undefined; |         if (!groups) return undefined; | ||||||
| 
 | 
 | ||||||
|         // Sort by original groupNumber |         // To "normalize" the group numbers, sort the groups and then renumber them | ||||||
|         const sortedGroups = [...groups].sort((a, b) => a.groupNumber - b.groupNumber); |         const renumbered = [...groups] | ||||||
| 
 |             .sort((a, b) => a.groupNumber - b.groupNumber) | ||||||
|         return sortedGroups |             .map((group, index) => ({ ...group, groupNo: index + 1 })); | ||||||
|             .map((group, index) => ({ |         return renumbered.find((group) => group.members?.some((m) => (m as StudentDTO).username === username.value)); | ||||||
|                 ...group, |  | ||||||
|                 groupNo: index + 1, // Renumbered index |  | ||||||
|             })) |  | ||||||
|             .find((group) => group.members?.some((m) => m.username === username.value)); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     watchEffect(() => { |     watchEffect(() => { | ||||||
|  | @ -89,7 +81,7 @@ | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
|         <using-query-result :query-result="assignmentsQueryResult"> |         <using-query-result :query-result="assignmentQueryResult"> | ||||||
|             <v-card |             <v-card | ||||||
|                 v-if="assignment" |                 v-if="assignment" | ||||||
|                 class="assignment-card" |                 class="assignment-card" | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import { computed, ref, watch, watchEffect } from "vue"; |     import { computed, ref, watchEffect } from "vue"; | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import { |     import { | ||||||
|         useAssignmentQuery, |         useAssignmentQuery, | ||||||
|  | @ -14,7 +14,7 @@ | ||||||
|     import type { GroupDTO, GroupDTOId } from "@dwengo-1/common/interfaces/group"; |     import type { GroupDTO, GroupDTOId } from "@dwengo-1/common/interfaces/group"; | ||||||
|     import GroupSubmissionStatus from "@/components/GroupSubmissionStatus.vue"; |     import GroupSubmissionStatus from "@/components/GroupSubmissionStatus.vue"; | ||||||
|     import GroupProgressRow from "@/components/GroupProgressRow.vue"; |     import GroupProgressRow from "@/components/GroupProgressRow.vue"; | ||||||
|     import type { AssignmentDTO } from "@dwengo-1/common/dist/interfaces/assignment.ts"; |     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||||
|     import GroupSelector from "@/components/assignments/GroupSelector.vue"; |     import GroupSelector from "@/components/assignments/GroupSelector.vue"; | ||||||
|     import DeadlineSelector from "@/components/assignments/DeadlineSelector.vue"; |     import DeadlineSelector from "@/components/assignments/DeadlineSelector.vue"; | ||||||
| 
 | 
 | ||||||
|  | @ -130,13 +130,28 @@ | ||||||
|         return `/learningPath/${lp.hruid}/${lp.language}/${lp.startNode.learningobjectHruid}?forGroup=${groupNo}&assignmentNo=${props.assignmentId}&classId=${props.classId}`; |         return `/learningPath/${lp.hruid}/${lp.language}/${lp.startNode.learningobjectHruid}?forGroup=${groupNo}&assignmentNo=${props.assignmentId}&classId=${props.classId}`; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const { mutate, data, isSuccess } = useUpdateAssignmentMutation(); |     const updateAssignmentMutate = useUpdateAssignmentMutation(); | ||||||
| 
 | 
 | ||||||
|     watch([isSuccess, data], async ([success, newData]) => { |     function updateAssignment(assignmentDTO): void { | ||||||
|         if (success && newData?.assignment) { |         updateAssignmentMutate.mutate( | ||||||
|  |             { | ||||||
|  |                 cid: assignmentQueryResult.data.value?.assignment.within, | ||||||
|  |                 an: assignmentQueryResult.data.value?.assignment.id, | ||||||
|  |                 data: assignmentDTO, | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 onSuccess: async (newData) => { | ||||||
|  |                     if (newData?.assignment) { | ||||||
|                         await assignmentQueryResult.refetch(); |                         await assignmentQueryResult.refetch(); | ||||||
|                     } |                     } | ||||||
|     }); |                 }, | ||||||
|  |                 onError: (err: any) => { | ||||||
|  |                     const message = err.response?.data?.error || err.message || t("unknownError"); | ||||||
|  |                     showSnackbar(t("failed") + ": " + message, "error"); | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     async function saveChanges(): Promise<void> { |     async function saveChanges(): Promise<void> { | ||||||
|         const { valid } = await form.value.validate(); |         const { valid } = await form.value.validate(); | ||||||
|  | @ -149,22 +164,14 @@ | ||||||
|             deadline: deadline.value ?? null, |             deadline: deadline.value ?? null, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         mutate({ |         updateAssignment(assignmentDTO); | ||||||
|             cid: assignmentQueryResult.data.value?.assignment.within, |  | ||||||
|             an: assignmentQueryResult.data.value?.assignment.id, |  | ||||||
|             data: assignmentDTO, |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> { |     async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> { | ||||||
|         const assignmentDTO: AssignmentDTO = { |         const assignmentDTO: AssignmentDTO = { | ||||||
|             groups: updatedGroups, |             groups: updatedGroups, | ||||||
|         }; |         }; | ||||||
|         mutate({ |         updateAssignment(assignmentDTO); | ||||||
|             cid: assignmentQueryResult.data.value?.assignment.within, |  | ||||||
|             an: assignmentQueryResult.data.value?.assignment.id, |  | ||||||
|             data: assignmentDTO, |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -401,13 +408,15 @@ | ||||||
| 
 | 
 | ||||||
|                                         <td> |                                         <td> | ||||||
|                                             <GroupSubmissionStatus |                                             <GroupSubmissionStatus | ||||||
|  |                                                 :learning-path-hruid="learningPath.hruid" | ||||||
|  |                                                 :language="lang" | ||||||
|                                                 :group="g" |                                                 :group="g" | ||||||
|                                                 :assignment-id="assignmentId" |                                                 :assignment-id="assignmentId" | ||||||
|                                                 :class-id="classId" |                                                 :class-id="classId" | ||||||
|                                                 :language="lang" |  | ||||||
|                                                 :go-to-group-submission-link="goToGroupSubmissionLink" |                                                 :go-to-group-submission-link="goToGroupSubmissionLink" | ||||||
|                                                 @update:hasSubmission=" |                                                 @update:hasSubmission=" | ||||||
|                                                     (hasSubmission) => (hasSubmissions = hasSubmission) |                                                     (hasSubmission) => | ||||||
|  |                                                         (hasSubmissions = hasSubmissions || hasSubmission) | ||||||
|                                                 " |                                                 " | ||||||
|                                             /> |                                             /> | ||||||
|                                         </td> |                                         </td> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import { ref, computed, onMounted, watch } from "vue"; |     import { ref, computed, onMounted } from "vue"; | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import { useRouter } from "vue-router"; |     import { useRouter } from "vue-router"; | ||||||
|     import authState from "@/services/auth/auth-service.ts"; |     import authState from "@/services/auth/auth-service.ts"; | ||||||
|  | @ -101,9 +101,9 @@ | ||||||
|         deleteAssignmentMutation.mutate( |         deleteAssignmentMutation.mutate( | ||||||
|             { cid: clsId, an: num }, |             { cid: clsId, an: num }, | ||||||
|             { |             { | ||||||
|                 onSuccess: (data) => { |                 onSuccess: async (data) => { | ||||||
|                     if (data?.assignment) { |                     if (data?.assignment) { | ||||||
|                         window.location.reload(); |                         await assignmentsQueryResult.refetch(); | ||||||
|                     } |                     } | ||||||
|                     showSnackbar(t("success"), "success"); |                     showSnackbar(t("success"), "success"); | ||||||
|                 }, |                 }, | ||||||
|  |  | ||||||
|  | @ -16,18 +16,14 @@ | ||||||
| 
 | 
 | ||||||
|     const groupsQuery = useGroupsQuery(props.classId, props.assignmentNumber, true); |     const groupsQuery = useGroupsQuery(props.classId, props.assignmentNumber, true); | ||||||
| 
 | 
 | ||||||
|     interface GroupSelectorOption { |     function sortedGroups(groups: GroupDTO[]): GroupDTO[] { | ||||||
|         groupNumber: number | undefined; |         return [...groups].sort((a, b) => a.groupNumber - b.groupNumber); | ||||||
|         label: string; |  | ||||||
|     } |     } | ||||||
| 
 |     function groupOptions(groups: GroupDTO[]): number[] { | ||||||
|     function groupOptions(groups: GroupDTO[]): GroupSelectorOption[] { |         return sortedGroups(groups).map((group) => group.groupNumber); | ||||||
|         return [...groups] |     } | ||||||
|             .sort((a, b) => a.groupNumber - b.groupNumber) |     function labelForGroup(groups: GroupDTO[], groupId: number): string { | ||||||
|             .map((group, index) => ({ |         return `${sortedGroups(groups).findIndex((group) => group.groupNumber === groupId) + 1}`; | ||||||
|                 groupNumber: group.groupNumber, |  | ||||||
|                 label: `${index + 1}`, |  | ||||||
|             })); |  | ||||||
|     } |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -40,7 +36,8 @@ | ||||||
|             :label="t('viewAsGroup')" |             :label="t('viewAsGroup')" | ||||||
|             :items="groupOptions(data.groups)" |             :items="groupOptions(data.groups)" | ||||||
|             v-model="model" |             v-model="model" | ||||||
|             item-title="label" |             :item-title="(item) => labelForGroup(data.groups, parseInt(`${item}`))" | ||||||
|  |             :item-value="(item) => item" | ||||||
|             class="group-selector-cb" |             class="group-selector-cb" | ||||||
|             variant="outlined" |             variant="outlined" | ||||||
|             clearable |             clearable | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ | ||||||
|     import authService from "@/services/auth/auth-service.ts"; |     import authService from "@/services/auth/auth-service.ts"; | ||||||
|     import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; |     import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; | ||||||
|     import LearningPathGroupSelector from "@/views/learning-paths/LearningPathGroupSelector.vue"; |     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 { QuestionsResponse } from "@/controllers/questions"; | ||||||
|     import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; |     import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||||
|     import QandA from "@/components/QandA.vue"; |     import QandA from "@/components/QandA.vue"; | ||||||
|  | @ -56,7 +56,12 @@ | ||||||
|     const learningObjectListQueryResult = useLearningObjectListForPathQuery(learningPathQueryResult.data); |     const learningObjectListQueryResult = useLearningObjectListForPathQuery(learningPathQueryResult.data); | ||||||
| 
 | 
 | ||||||
|     const nodesList: ComputedRef<LearningPathNode[] | null> = computed( |     const nodesList: ComputedRef<LearningPathNode[] | null> = computed( | ||||||
|         () => learningPathQueryResult.data.value?.nodesAsList ?? null, |         () => | ||||||
|  |             learningPathQueryResult.data.value?.nodesAsList.filter( | ||||||
|  |                 (node) => | ||||||
|  |                     authService.authState.activeRole === AccountType.Teacher || | ||||||
|  |                     !getLearningObjectForNode(node)?.teacherExclusive, | ||||||
|  |             ) ?? null, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const currentNode = computed(() => { |     const currentNode = computed(() => { | ||||||
|  | @ -76,7 +81,24 @@ | ||||||
|         return currentIndex < nodesList.value?.length ? nodesList.value?.[currentIndex - 1] : undefined; |         return currentIndex < nodesList.value?.length ? nodesList.value?.[currentIndex - 1] : undefined; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const getQuestionsQuery = useQuestionsQuery( |     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( |             computed( | ||||||
|                 () => |                 () => | ||||||
|                     ({ |                     ({ | ||||||
|  | @ -86,9 +108,17 @@ | ||||||
|                     }) as LearningObjectIdentifierDTO, |                     }) as LearningObjectIdentifierDTO, | ||||||
|             ), |             ), | ||||||
|         ); |         ); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     const navigationDrawerShown = ref(true); |     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 { |     function isLearningObjectCompleted(learningObject: LearningObject): boolean { | ||||||
|         if (learningObjectListQueryResult.isSuccess) { |         if (learningObjectListQueryResult.isSuccess) { | ||||||
|             return ( |             return ( | ||||||
|  | @ -155,6 +185,20 @@ | ||||||
|             "/" + |             "/" + | ||||||
|             currentNode.value?.learningobjectHruid, |             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, | ||||||
|  |             ) ?? [] | ||||||
|  |         ); | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -265,7 +309,7 @@ | ||||||
|                 </v-list-item> |                 </v-list-item> | ||||||
|                 <v-list-item> |                 <v-list-item> | ||||||
|                     <div |                     <div | ||||||
|                         v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment" |                         v-if="authService.authState.activeRole === AccountType.Student && forGroup" | ||||||
|                         class="assignment-indicator" |                         class="assignment-indicator" | ||||||
|                     > |                     > | ||||||
|                         {{ t("assignmentIndicator") }} |                         {{ t("assignmentIndicator") }} | ||||||
|  | @ -312,7 +356,7 @@ | ||||||
|             </v-btn> |             </v-btn> | ||||||
|         </div> |         </div> | ||||||
|         <using-query-result |         <using-query-result | ||||||
|             v-if="forGroup" |             v-if="currentNode && forGroup" | ||||||
|             :query-result="getQuestionsQuery" |             :query-result="getQuestionsQuery" | ||||||
|             v-slot="questionsResponse: { data: QuestionsResponse }" |             v-slot="questionsResponse: { data: QuestionsResponse }" | ||||||
|         > |         > | ||||||
|  | @ -327,12 +371,16 @@ | ||||||
|                 </span> |                 </span> | ||||||
|             </div> |             </div> | ||||||
|             <QuestionBox |             <QuestionBox | ||||||
|                 :hruid="props.hruid" |                 :learningObjectHruid="currentNode.learningobjectHruid" | ||||||
|                 :language="props.language" |                 :learningObjectLanguage="currentNode.language" | ||||||
|                 :learningObjectHruid="props.learningObjectHruid" |                 :learningObjectVersion="currentNode.version" | ||||||
|                 :forGroup="forGroup" |                 :forGroup="{ | ||||||
|  |                     assignment: forGroup.assignmentNo, | ||||||
|  |                     class: forGroup.classId, | ||||||
|  |                     groupNumber: forGroup.forGroup, | ||||||
|  |                 }" | ||||||
|             /> |             /> | ||||||
|             <QandA :questions="(questionsResponse.data.questions as QuestionDTO[]) ?? []" /> |             <QandA :questions="filterQuestions(questionsResponse.data.questions as QuestionDTO[])" /> | ||||||
|         </using-query-result> |         </using-query-result> | ||||||
|     </using-query-result> |     </using-query-result> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl