Merge pull request #289 from SELab-2/fix/questions-toon-enkel-groep

fix: Questions, submissions & discussions
This commit is contained in:
Gabriellvl 2025-05-20 20:57:19 +02:00 committed by GitHub
commit d68564c953
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 280 additions and 188 deletions

View file

@ -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<T extends object> extends EntityRepository<T> {
public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> {
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<T>): Promise<void> {
const toDelete = await this.findOne(query);

View file

@ -18,13 +18,8 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
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<Question[]> {

View file

@ -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;

View file

@ -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);
});

View file

@ -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;
}

View file

@ -1,11 +1,13 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import UsingQueryResult from "@/components/UsingQueryResult.vue";
import { useAssignmentSubmissionsQuery } from "@/queries/assignments.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<{
learningPathHruid: string;
language: string;
group: object;
assignmentId: number;
classId: string;
@ -15,18 +17,24 @@
const emit = defineEmits<(e: "update:hasSubmission", hasSubmission: boolean) => void>();
const { t } = useI18n();
const submissionsQuery = useAssignmentSubmissionsQuery(
() => props.classId,
() => props.assignmentId,
() => props.group.originalGroupNo,
() => true,
const hasMadeProgress = ref(false);
const getLearningPathQuery = useGetLearningPathQuery(
() => props.learningPathHruid,
() => props.language,
() => ({
forGroup: props.group.originalGroupNo,
assignmentNo: props.assignmentId,
classId: props.classId,
}),
);
watch(
() => submissionsQuery.data.value,
(data) => {
if (data) {
emit("update:hasSubmission", data.submissions.length > 0);
() => getLearningPathQuery.data.value,
(learningPath) => {
if (learningPath) {
hasMadeProgress.value = learningPath.amountOfNodes !== learningPath.amountOfNodesLeft;
emit("update:hasSubmission", hasMadeProgress.value);
}
},
{ immediate: true },
@ -35,16 +43,16 @@
<template>
<using-query-result
:query-result="submissionsQuery"
:query-result="getLearningPathQuery"
v-slot="{ data }: { data: SubmissionsResponse }"
>
<v-btn
:color="data?.submissions?.length > 0 ? 'green' : 'red'"
:color="hasMadeProgress ? 'green' : 'red'"
variant="text"
:to="data.submissions.length > 0 ? goToGroupSubmissionLink(props.group.groupNo) : undefined"
:disabled="data.submissions.length === 0"
:to="hasMadeProgress ? goToGroupSubmissionLink(props.group.originalGroupNo) : undefined"
:disabled="!hasMadeProgress"
>
{{ data.submissions.length > 0 ? t("submission") : t("noSubmissionsYet") }}
{{ hasMadeProgress ? t("submission") : t("noSubmissionsYet") }}
</v-btn>
</using-query-result>
</template>

View file

@ -1,92 +1,47 @@
<script setup lang="ts">
import authService from "@/services/auth/auth-service.ts";
import { Language } from "@/data-objects/language.ts";
import { computed, type ComputedRef, ref } from "vue";
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
import { useStudentAssignmentsQuery, useStudentGroupsQuery } from "@/queries/students.ts";
import type { GroupDTO, GroupDTOId } from "@dwengo-1/common/interfaces/group";
import type { GroupDTOId } from "@dwengo-1/common/interfaces/group";
import type { QuestionData } from "@dwengo-1/common/interfaces/question";
import type { LearningObjectIdentifierDTO } from "@dwengo-1/interfaces/learning-content";
import { useCreateQuestionMutation, useQuestionsQuery } 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 { useCreateQuestionMutation } from "@/queries/questions.ts";
import { useI18n } from "vue-i18n";
import { AccountType } from "@dwengo-1/common/util/account-types.ts";
const props = defineProps<{
hruid: string;
language: Language;
learningObjectHruid?: string;
learningObjectHruid: string;
learningObjectLanguage: string;
learningObjectVersion: number;
forGroup?: GroupDTOId | undefined;
withTitle?: boolean;
}>();
const { t } = useI18n();
const studentAssignmentsQueryResult = useStudentAssignmentsQuery(
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 emit = defineEmits(["updated"]);
const questionInput = ref("");
const loID: ComputedRef<LearningObjectIdentifierDTO> = computed(() => ({
hruid: props.learningObjectHruid as string,
language: props.language,
language: props.learningObjectLanguage,
version: props.learningObjectVersion,
}));
const createQuestionMutation = useCreateQuestionMutation(loID);
const groupsQueryResult = useStudentGroupsQuery(authService.authState.user?.profile.preferred_username);
const showQuestionBox = computed(
() => authService.authState.activeRole === AccountType.Student && pathIsAssignment.value,
);
const showQuestionBox = computed(() => authService.authState.activeRole === AccountType.Student && props.forGroup);
function submitQuestion(): void {
const assignments = studentAssignmentsQueryResult.data.value?.assignments as AssignmentDTO[];
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 = {
author: authService.authState.user?.profile.preferred_username,
content: questionInput.value,
inGroup: group,
};
if (questionInput.value !== "") {
if (props.forGroup && questionInput.value !== "") {
const questionData: QuestionData = {
author: authService.authState.user?.profile.preferred_username,
content: questionInput.value,
inGroup: props.forGroup,
};
createQuestionMutation.mutate(questionData, {
onSuccess: async () => {
questionInput.value = ""; // Clear the input field after submission
await getQuestionsQuery.refetch(); // Reload the questions
emit("updated");
},
onError: (_) => {
// TODO Handle error

View file

@ -48,4 +48,12 @@ export class AssignmentController extends BaseController {
async getGroups(assignmentNumber: number, full = true): Promise<GroupsResponse> {
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 });
}
}

View file

@ -18,6 +18,15 @@ export class QuestionController extends BaseController {
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> {
return this.get<QuestionsResponse>("/", { lang: this.loId.language, full });
}

View file

@ -23,7 +23,14 @@ export class SubmissionController extends BaseController {
groupId?: number,
full = true,
): 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(
@ -39,7 +46,7 @@ export class SubmissionController extends BaseController {
version,
classId,
assignmentId,
groupId,
forGroup: groupId,
});
}

View file

@ -181,7 +181,7 @@ export function useAssignmentSubmissionsQuery(
return useQuery({
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),
});
}

View file

@ -9,6 +9,7 @@ import {
useQueryClient,
type UseQueryReturnType,
} from "@tanstack/vue-query";
import type { Language } from "@dwengo-1/common/util/language";
export function questionsQueryKey(
loId: LearningObjectIdentifierDTO,
@ -17,6 +18,16 @@ export function questionsQueryKey(
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] {
const loId = questionId.learningObjectIdentifier;
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(
questionId: MaybeRefOrGetter<QuestionId>,
): UseQueryReturnType<QuestionResponse, Error> {

View file

@ -62,7 +62,10 @@
const { valid } = await form.value.validate();
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) {
return;
}
@ -72,8 +75,8 @@
within: selectedClass.value?.id || "",
title: assignmentTitle.value,
description: "",
learningPath: lp,
language: language.value,
learningPath: lp.hruid,
language: lp.language,
deadline: null,
groups: [],
};

View file

@ -1,18 +1,18 @@
<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 { useI18n } from "vue-i18n";
import UsingQueryResult from "@/components/UsingQueryResult.vue";
import { asyncComputed } from "@vueuse/core";
import {
useStudentAssignmentsQuery,
useStudentGroupsQuery,
useStudentsByUsernamesQuery,
} from "@/queries/students.ts";
import { useStudentsByUsernamesQuery } from "@/queries/students.ts";
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
import type { Language } from "@/data-objects/language.ts";
import { calculateProgress } from "@/utils/assignment-utils.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<{
classId: string;
@ -28,32 +28,24 @@
return user?.profile?.preferred_username ?? undefined;
});
const assignmentsQueryResult = useStudentAssignmentsQuery(username, true);
const assignmentQueryResult = useAssignmentQuery(props.classId, props.assignmentId);
const assignment = computed(() => {
const assignments = assignmentsQueryResult.data.value?.assignments;
if (!assignments) return undefined;
return assignments.find((a) => a.id === props.assignmentId && a.within === props.classId);
});
const assignment: ComputedRef<AssignmentDTO | undefined> = computed(
() => assignmentQueryResult.data.value?.assignment,
);
learningPath.value = assignment.value?.learningPath;
const groupsQueryResult = useStudentGroupsQuery(username, true);
const group = computed(() => {
const groups = groupsQueryResult.data.value?.groups as GroupDTO[];
const groups = assignment.value?.groups as GroupDTO[];
if (!groups) return undefined;
// Sort by original groupNumber
const sortedGroups = [...groups].sort((a, b) => a.groupNumber - b.groupNumber);
return sortedGroups
.map((group, index) => ({
...group,
groupNo: index + 1, // Renumbered index
}))
.find((group) => group.members?.some((m) => m.username === username.value));
// To "normalize" the group numbers, sort the groups and then renumber them
const renumbered = [...groups]
.sort((a, b) => a.groupNumber - b.groupNumber)
.map((group, index) => ({ ...group, groupNo: index + 1 }));
return renumbered.find((group) => group.members?.some((m) => (m as StudentDTO).username === username.value));
});
watchEffect(() => {
@ -89,7 +81,7 @@
<template>
<div class="container">
<using-query-result :query-result="assignmentsQueryResult">
<using-query-result :query-result="assignmentQueryResult">
<v-card
v-if="assignment"
class="assignment-card"

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, watch, watchEffect } from "vue";
import { computed, ref, watchEffect } from "vue";
import { useI18n } from "vue-i18n";
import {
useAssignmentQuery,
@ -14,7 +14,7 @@
import type { GroupDTO, GroupDTOId } from "@dwengo-1/common/interfaces/group";
import GroupSubmissionStatus from "@/components/GroupSubmissionStatus.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 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}`;
}
const { mutate, data, isSuccess } = useUpdateAssignmentMutation();
const updateAssignmentMutate = useUpdateAssignmentMutation();
watch([isSuccess, data], async ([success, newData]) => {
if (success && newData?.assignment) {
await assignmentQueryResult.refetch();
}
});
function updateAssignment(assignmentDTO): void {
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();
}
},
onError: (err: any) => {
const message = err.response?.data?.error || err.message || t("unknownError");
showSnackbar(t("failed") + ": " + message, "error");
},
},
);
}
async function saveChanges(): Promise<void> {
const { valid } = await form.value.validate();
@ -149,22 +164,14 @@
deadline: deadline.value ?? null,
};
mutate({
cid: assignmentQueryResult.data.value?.assignment.within,
an: assignmentQueryResult.data.value?.assignment.id,
data: assignmentDTO,
});
updateAssignment(assignmentDTO);
}
async function handleGroupsUpdated(updatedGroups: string[][]): Promise<void> {
const assignmentDTO: AssignmentDTO = {
groups: updatedGroups,
};
mutate({
cid: assignmentQueryResult.data.value?.assignment.within,
an: assignmentQueryResult.data.value?.assignment.id,
data: assignmentDTO,
});
updateAssignment(assignmentDTO);
}
</script>
@ -401,13 +408,15 @@
<td>
<GroupSubmissionStatus
:learning-path-hruid="learningPath.hruid"
:language="lang"
:group="g"
:assignment-id="assignmentId"
:class-id="classId"
:language="lang"
:go-to-group-submission-link="goToGroupSubmissionLink"
@update:hasSubmission="
(hasSubmission) => (hasSubmissions = hasSubmission)
(hasSubmission) =>
(hasSubmissions = hasSubmissions || hasSubmission)
"
/>
</td>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch } from "vue";
import { ref, computed, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import authState from "@/services/auth/auth-service.ts";
@ -101,9 +101,9 @@
deleteAssignmentMutation.mutate(
{ cid: clsId, an: num },
{
onSuccess: (data) => {
onSuccess: async (data) => {
if (data?.assignment) {
window.location.reload();
await assignmentsQueryResult.refetch();
}
showSnackbar(t("success"), "success");
},

View file

@ -16,18 +16,14 @@
const groupsQuery = useGroupsQuery(props.classId, props.assignmentNumber, true);
interface GroupSelectorOption {
groupNumber: number | undefined;
label: string;
function sortedGroups(groups: GroupDTO[]): GroupDTO[] {
return [...groups].sort((a, b) => a.groupNumber - b.groupNumber);
}
function groupOptions(groups: GroupDTO[]): GroupSelectorOption[] {
return [...groups]
.sort((a, b) => a.groupNumber - b.groupNumber)
.map((group, index) => ({
groupNumber: group.groupNumber,
label: `${index + 1}`,
}));
function groupOptions(groups: GroupDTO[]): number[] {
return sortedGroups(groups).map((group) => group.groupNumber);
}
function labelForGroup(groups: GroupDTO[], groupId: number): string {
return `${sortedGroups(groups).findIndex((group) => group.groupNumber === groupId) + 1}`;
}
</script>
@ -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

View file

@ -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<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(() => {
@ -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,
) ?? []
);
}
</script>
<template>
@ -265,7 +309,7 @@
</v-list-item>
<v-list-item>
<div
v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment"
v-if="authService.authState.activeRole === AccountType.Student && forGroup"
class="assignment-indicator"
>
{{ t("assignmentIndicator") }}
@ -312,7 +356,7 @@
</v-btn>
</div>
<using-query-result
v-if="forGroup"
v-if="currentNode && forGroup"
:query-result="getQuestionsQuery"
v-slot="questionsResponse: { data: QuestionsResponse }"
>
@ -327,12 +371,16 @@
</span>
</div>
<QuestionBox
:hruid="props.hruid"
:language="props.language"
:learningObjectHruid="props.learningObjectHruid"
:forGroup="forGroup"
:learningObjectHruid="currentNode.learningobjectHruid"
:learningObjectLanguage="currentNode.language"
:learningObjectVersion="currentNode.version"
: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>
</template>