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 { 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);
|
||||
|
|
|
@ -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[]> {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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: [],
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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");
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue