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.`);
|
||||||
}
|
}
|
||||||
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> {
|
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,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 studentLists = await Promise.all((assignmentData.groups as string[][]).map(async (group) => await fetchStudents(group)));
|
||||||
|
|
||||||
const groupRepository = getGroupRepository();
|
try {
|
||||||
await groupRepository.deleteAllByAssignment(assignment);
|
const groupRepository = getGroupRepository();
|
||||||
await Promise.all(
|
await groupRepository.deleteAllByAssignment(assignment);
|
||||||
studentLists.map(async (students) => {
|
|
||||||
const newGroup = groupRepository.create({
|
await Promise.all(
|
||||||
assignment: assignment,
|
studentLists.map(async (students) => {
|
||||||
members: students,
|
const newGroup = groupRepository.create({
|
||||||
});
|
assignment: assignment,
|
||||||
await groupRepository.save(newGroup);
|
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;
|
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(
|
const questionData: QuestionData = {
|
||||||
(assignment) => assignment.learningPath === props.hruid && assignment.language === props.language,
|
author: authService.authState.user?.profile.preferred_username,
|
||||||
);
|
content: questionInput.value,
|
||||||
const groups = groupsQueryResult.data.value?.groups as GroupDTO[];
|
inGroup: props.forGroup,
|
||||||
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 !== "") {
|
|
||||||
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(
|
||||||
await assignmentQueryResult.refetch();
|
{
|
||||||
}
|
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> {
|
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,19 +81,44 @@
|
||||||
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;
|
||||||
computed(
|
|
||||||
() =>
|
if (authService.authState.activeRole === AccountType.Student) {
|
||||||
({
|
getQuestionsQuery = useQuestionsGroupQuery(
|
||||||
language: currentNode.value?.language,
|
computed(
|
||||||
hruid: currentNode.value?.learningobjectHruid,
|
() =>
|
||||||
version: currentNode.value?.version,
|
({
|
||||||
}) as LearningObjectIdentifierDTO,
|
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);
|
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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue