feat(backend): De meest recente indiening wordt automatisch ingeladen.
This commit is contained in:
parent
1685c518b6
commit
63c3d6aaa0
18 changed files with 406 additions and 263 deletions
|
@ -1,5 +1,5 @@
|
||||||
import { Submission } from '../entities/assignments/submission.entity.js';
|
import { Submission } from '../entities/assignments/submission.entity.js';
|
||||||
import { mapToGroupDTO } from './group.js';
|
import { mapToGroupDTOId } from './group.js';
|
||||||
import { mapToStudentDTO } from './student.js';
|
import { mapToStudentDTO } from './student.js';
|
||||||
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
|
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
|
||||||
import { getSubmissionRepository } from '../data/repositories';
|
import { getSubmissionRepository } from '../data/repositories';
|
||||||
|
@ -13,11 +13,10 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
||||||
language: submission.learningObjectLanguage,
|
language: submission.learningObjectLanguage,
|
||||||
version: submission.learningObjectVersion,
|
version: submission.learningObjectVersion,
|
||||||
},
|
},
|
||||||
|
|
||||||
submissionNumber: submission.submissionNumber,
|
submissionNumber: submission.submissionNumber,
|
||||||
submitter: mapToStudentDTO(submission.submitter),
|
submitter: mapToStudentDTO(submission.submitter),
|
||||||
time: submission.submissionTime,
|
time: submission.submissionTime,
|
||||||
group: mapToGroupDTO(submission.onBehalfOf),
|
group: mapToGroupDTOId(submission.onBehalfOf),
|
||||||
content: submission.content,
|
content: submission.content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export function errorHandler(err: unknown, _req: Request, res: Response, _: Next
|
||||||
logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`);
|
logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`);
|
||||||
res.status(err.status).json(err);
|
res.status(err.status).json(err);
|
||||||
} else {
|
} else {
|
||||||
logger.error(`Unexpected error occurred while handing a request: ${JSON.stringify(err)}`);
|
logger.error(`Unexpected error occurred while handing a request: ${(err as {stack: string})?.stack ?? JSON.stringify(err)}`);
|
||||||
res.status(500).json(err);
|
res.status(500).json(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"welcome": "Willkommen",
|
"welcome": "Willkommen",
|
||||||
"student": "schüler",
|
"student": "Schüler",
|
||||||
"teacher": "lehrer",
|
"teacher": "Lehrer",
|
||||||
"assignments": "Aufgaben",
|
"assignments": "Aufgaben",
|
||||||
"classes": "Klasses",
|
"classes": "Klassen",
|
||||||
"discussions": "Diskussionen",
|
"discussions": "Diskussionen",
|
||||||
"login": "einloggen",
|
"login": "einloggen",
|
||||||
"logout": "ausloggen",
|
"logout": "ausloggen",
|
||||||
"cancel": "kündigen",
|
"cancel": "abbrechen",
|
||||||
"logoutVerification": "Sind Sie sicher, dass Sie sich abmelden wollen?",
|
"logoutVerification": "Sind Sie sicher, dass Sie sich abmelden wollen?",
|
||||||
"homeTitle": "Unsere Stärken",
|
"homeTitle": "Unsere Stärken",
|
||||||
"homeIntroduction1": "Wir entwickeln innovative Workshops und Bildungsressourcen, die wir in Zusammenarbeit mit Lehrern und Freiwilligen Schülern auf der ganzen Welt zur Verfügung stellen. Unsere Train-the-Trainer-Sitzungen ermöglichen es ihnen, unsere praktischen Workshops an die Schüler weiterzugeben.",
|
"homeIntroduction1": "Wir entwickeln innovative Workshops und Bildungsressourcen, die wir in Zusammenarbeit mit Lehrern und Freiwilligen Schülern auf der ganzen Welt zur Verfügung stellen. Unsere Train-the-Trainer-Sitzungen ermöglichen es ihnen, unsere praktischen Workshops an die Schüler weiterzugeben.",
|
||||||
|
@ -23,10 +23,10 @@
|
||||||
"submitCode": "senden",
|
"submitCode": "senden",
|
||||||
"members": "Mitglieder",
|
"members": "Mitglieder",
|
||||||
"themes": "Themen",
|
"themes": "Themen",
|
||||||
"choose-theme": "Wähle ein thema",
|
"choose-theme": "Wählen Sie ein Thema",
|
||||||
"choose-age": "Alter auswählen",
|
"choose-age": "Alter auswählen",
|
||||||
"theme-options": {
|
"theme-options": {
|
||||||
"all": "Alle themen",
|
"all": "Alle Themen",
|
||||||
"culture": "Kultur",
|
"culture": "Kultur",
|
||||||
"electricity-and-mechanics": "Elektrizität und Mechanik",
|
"electricity-and-mechanics": "Elektrizität und Mechanik",
|
||||||
"nature-and-climate": "Natur und Klima",
|
"nature-and-climate": "Natur und Klima",
|
||||||
|
@ -37,11 +37,11 @@
|
||||||
"algorithms": "Algorithmisches Denken"
|
"algorithms": "Algorithmisches Denken"
|
||||||
},
|
},
|
||||||
"age-options": {
|
"age-options": {
|
||||||
"all": "Alle altersgruppen",
|
"all": "Alle Altersgruppen",
|
||||||
"primary-school": "Grundschule",
|
"primary-school": "Grundschule",
|
||||||
"lower-secondary": "12-14 jahre alt",
|
"lower-secondary": "12-14 Jahre alt",
|
||||||
"upper-secondary": "14-16 jahre alt",
|
"upper-secondary": "14-16 Jahre alt",
|
||||||
"high-school": "16-18 jahre alt",
|
"high-school": "16-18 Jahre alt",
|
||||||
"older": "18 und älter"
|
"older": "18 und älter"
|
||||||
},
|
},
|
||||||
"read-more": "Mehr lesen",
|
"read-more": "Mehr lesen",
|
||||||
|
@ -73,7 +73,9 @@
|
||||||
"accept": "akzeptieren",
|
"accept": "akzeptieren",
|
||||||
"deny": "ablehnen",
|
"deny": "ablehnen",
|
||||||
"sent": "sent",
|
"sent": "sent",
|
||||||
"failed": "gescheitert",
|
"failed": "fehlgeschlagen",
|
||||||
"wrong": "etwas ist schief gelaufen",
|
"wrong": "etwas ist schief gelaufen",
|
||||||
"created": "erstellt"
|
"created": "erstellt",
|
||||||
|
"submit": "Einreichen",
|
||||||
|
"markAsDone": "Als fertig markieren"
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,5 +75,7 @@
|
||||||
"sent": "sent",
|
"sent": "sent",
|
||||||
"failed": "failed",
|
"failed": "failed",
|
||||||
"wrong": "something went wrong",
|
"wrong": "something went wrong",
|
||||||
"created": "created"
|
"created": "created",
|
||||||
|
"submit": "Submit",
|
||||||
|
"markAsDone": "Mark as done"
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,5 +75,7 @@
|
||||||
"sent": "verzonden",
|
"sent": "verzonden",
|
||||||
"failed": "mislukt",
|
"failed": "mislukt",
|
||||||
"wrong": "er ging iets verkeerd",
|
"wrong": "er ging iets verkeerd",
|
||||||
"created": "gecreëerd"
|
"created": "gecreëerd",
|
||||||
|
"submit": "Indienen",
|
||||||
|
"markAsDone": "Markeren als afgewerkt"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,11 @@ const learningPathController = getLearningPathController();
|
||||||
export function useGetLearningPathQuery(
|
export function useGetLearningPathQuery(
|
||||||
hruid: MaybeRefOrGetter<string>,
|
hruid: MaybeRefOrGetter<string>,
|
||||||
language: MaybeRefOrGetter<Language>,
|
language: MaybeRefOrGetter<Language>,
|
||||||
forGroup?: MaybeRefOrGetter<{forGroup: number, assignmentNo: number, classId: string}>,
|
forGroup?: MaybeRefOrGetter<{forGroup: number, assignmentNo: number, classId: string} | undefined>,
|
||||||
): UseQueryReturnType<LearningPath, Error> {
|
): UseQueryReturnType<LearningPath, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [LEARNING_PATH_KEY, "get", toValue(hruid), toValue(language), toValue(forGroup)],
|
queryKey: [LEARNING_PATH_KEY, "get", toValue(hruid), toValue(language), toValue(forGroup)],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
console.log("queryKey");
|
|
||||||
console.log([LEARNING_PATH_KEY, "get", toValue(hruid), toValue(language), toValue(forGroup)]);
|
|
||||||
const [hruidVal, languageVal, forGroupVal] = [toValue(hruid), toValue(language), toValue(forGroup)];
|
const [hruidVal, languageVal, forGroupVal] = [toValue(hruid), toValue(language), toValue(forGroup)];
|
||||||
return learningPathController.getBy(hruidVal, languageVal, forGroupVal);
|
return learningPathController.getBy(hruidVal, languageVal, forGroupVal);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SubmissionController, type SubmissionResponse, type SubmissionsResponse } from "@/controllers/submissions";
|
import { SubmissionController, type SubmissionResponse } from "@/controllers/submissions";
|
||||||
import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
|
import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
|
||||||
import {
|
import {
|
||||||
QueryClient,
|
QueryClient,
|
||||||
|
@ -13,17 +13,7 @@ import {LEARNING_PATH_KEY} from "@/queries/learning-paths.ts";
|
||||||
import {LEARNING_OBJECT_KEY} from "@/queries/learning-objects.ts";
|
import {LEARNING_OBJECT_KEY} from "@/queries/learning-objects.ts";
|
||||||
import type {Language} from "@dwengo-1/common/util/language";
|
import type {Language} from "@dwengo-1/common/util/language";
|
||||||
|
|
||||||
function submissionsQueryKey(
|
export const SUBMISSION_KEY = "submissions";
|
||||||
hruid: string,
|
|
||||||
language: Language,
|
|
||||||
version: number,
|
|
||||||
classid: string,
|
|
||||||
assignmentNumber: number,
|
|
||||||
groupNumber?: number,
|
|
||||||
full?: boolean
|
|
||||||
) {
|
|
||||||
return ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber, full ?? false];
|
|
||||||
}
|
|
||||||
|
|
||||||
function submissionQueryKey(
|
function submissionQueryKey(
|
||||||
hruid: string,
|
hruid: string,
|
||||||
|
@ -72,19 +62,8 @@ export async function invalidateAllSubmissionKeys(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkEnabled(
|
function checkEnabled(properties: MaybeRefOrGetter<unknown>[]): boolean {
|
||||||
classid: string | undefined,
|
return properties.every(prop => !!toValue(prop));
|
||||||
assignmentNumber: number | undefined,
|
|
||||||
groupNumber: number | undefined,
|
|
||||||
submissionNumber?: number | undefined,
|
|
||||||
submissionNumberRequired: boolean = false
|
|
||||||
): boolean {
|
|
||||||
return (
|
|
||||||
Boolean(classid) &&
|
|
||||||
!isNaN(Number(groupNumber)) &&
|
|
||||||
!isNaN(Number(assignmentNumber)) &&
|
|
||||||
(!isNaN(Number(submissionNumber)) || !submissionNumberRequired)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toValues(
|
function toValues(
|
||||||
|
@ -110,7 +89,10 @@ export function useSubmissionsQuery(
|
||||||
assignmentNumber: MaybeRefOrGetter<number | undefined>,
|
assignmentNumber: MaybeRefOrGetter<number | undefined>,
|
||||||
groupNumber: MaybeRefOrGetter<number | undefined>,
|
groupNumber: MaybeRefOrGetter<number | undefined>,
|
||||||
full: MaybeRefOrGetter<boolean> = true,
|
full: MaybeRefOrGetter<boolean> = true,
|
||||||
): UseQueryReturnType<SubmissionsResponse, Error> {
|
): UseQueryReturnType<SubmissionDTO[], Error> {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber, full],
|
||||||
|
queryFn: async () => {
|
||||||
const hruidVal = toValue(hruid);
|
const hruidVal = toValue(hruid);
|
||||||
const languageVal = toValue(language);
|
const languageVal = toValue(language);
|
||||||
const versionVal = toValue(version);
|
const versionVal = toValue(version);
|
||||||
|
@ -119,22 +101,12 @@ export function useSubmissionsQuery(
|
||||||
const groupNumberVal = toValue(groupNumber);
|
const groupNumberVal = toValue(groupNumber);
|
||||||
const fullVal = toValue(full);
|
const fullVal = toValue(full);
|
||||||
|
|
||||||
return useQuery({
|
const response = await new SubmissionController(hruidVal!).getAll(
|
||||||
queryKey: computed(() =>
|
|
||||||
submissionsQueryKey(
|
|
||||||
hruidVal!,
|
|
||||||
languageVal!,
|
|
||||||
versionVal!,
|
|
||||||
classIdVal!,
|
|
||||||
assignmentNumberVal!,
|
|
||||||
groupNumberVal,
|
|
||||||
fullVal
|
|
||||||
)
|
|
||||||
),
|
|
||||||
queryFn: async () => new SubmissionController(hruidVal!).getAll(
|
|
||||||
languageVal!, versionVal!, classIdVal!, assignmentNumberVal!, groupNumberVal, fullVal
|
languageVal!, versionVal!, classIdVal!, assignmentNumberVal!, groupNumberVal, fullVal
|
||||||
),
|
);
|
||||||
enabled: () => !!hruidVal && !!languageVal && !!versionVal && !!classIdVal && !!assignmentNumberVal,
|
return response ? response.submissions as SubmissionDTO[] : undefined;
|
||||||
|
},
|
||||||
|
enabled: () => checkEnabled([hruid, language, version, classid, assignmentNumber]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,8 +119,6 @@ export function useSubmissionQuery(
|
||||||
groupNumber: MaybeRefOrGetter<number | undefined>,
|
groupNumber: MaybeRefOrGetter<number | undefined>,
|
||||||
submissionNumber: MaybeRefOrGetter<number | undefined>,
|
submissionNumber: MaybeRefOrGetter<number | undefined>,
|
||||||
): UseQueryReturnType<SubmissionResponse, Error> {
|
): UseQueryReturnType<SubmissionResponse, Error> {
|
||||||
const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, submissionNumber, true);
|
|
||||||
|
|
||||||
const hruidVal = toValue(hruid);
|
const hruidVal = toValue(hruid);
|
||||||
const languageVal = toValue(language);
|
const languageVal = toValue(language);
|
||||||
const versionVal = toValue(version);
|
const versionVal = toValue(version);
|
||||||
|
@ -192,11 +162,6 @@ export function useCreateSubmissionMutation(): UseMutationReturnType<
|
||||||
const {hruid, language, version} = response.submission.learningObjectIdentifier;
|
const {hruid, language, version} = response.submission.learningObjectIdentifier;
|
||||||
await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn);
|
await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn);
|
||||||
|
|
||||||
console.log("INVALIDATE");
|
|
||||||
console.log([
|
|
||||||
LEARNING_PATH_KEY, "get",
|
|
||||||
response.submission.learningObjectIdentifier.hruid,
|
|
||||||
]);
|
|
||||||
await queryClient.invalidateQueries({queryKey: [LEARNING_PATH_KEY, "get"]});
|
await queryClient.invalidateQueries({queryKey: [LEARNING_PATH_KEY, "get"]});
|
||||||
|
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
|
@ -216,7 +181,7 @@ export function useDeleteSubmissionMutation(): UseMutationReturnType<
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ cid, an, gn, sn }) => new SubmissionController(cid).deleteSubmission(sn),
|
mutationFn: async ({ cid, sn }) => new SubmissionController(cid).deleteSubmission(sn),
|
||||||
onSuccess: async (response) => {
|
onSuccess: async (response) => {
|
||||||
if (!response.submission.group) {
|
if (!response.submission.group) {
|
||||||
await invalidateAllSubmissionKeys(queryClient);
|
await invalidateAllSubmissionKeys(queryClient);
|
||||||
|
|
|
@ -15,7 +15,7 @@ import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue";
|
||||||
import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue";
|
import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue";
|
||||||
import UserHomePage from "@/views/homepage/UserHomePage.vue";
|
import UserHomePage from "@/views/homepage/UserHomePage.vue";
|
||||||
import SingleTheme from "@/views/SingleTheme.vue";
|
import SingleTheme from "@/views/SingleTheme.vue";
|
||||||
import LearningObjectView from "@/views/learning-paths/LearningObjectView.vue";
|
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
|
5
frontend/src/utils/array-utils.ts
Normal file
5
frontend/src/utils/array-utils.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export function copyArrayWith<T>(index: number, newValue: T, array: T[]) {
|
||||||
|
const copy = [...array];
|
||||||
|
copy[index] = newValue;
|
||||||
|
return copy;
|
||||||
|
}
|
20
frontend/src/utils/deep-equals.ts
Normal file
20
frontend/src/utils/deep-equals.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
export function deepEquals<T>(a: T, b: T): boolean {
|
||||||
|
if (a === b) return true;
|
||||||
|
|
||||||
|
if (typeof a !== 'object' || typeof b !== 'object' || a == null || b == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||||
|
|
||||||
|
if (Array.isArray(a) && Array.isArray(b)) {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
return a.every((val, i) => deepEquals(val, b[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysA = Object.keys(a) as (keyof T)[];
|
||||||
|
const keysB = Object.keys(b) as (keyof T)[];
|
||||||
|
|
||||||
|
if (keysA.length !== keysB.length) return false;
|
||||||
|
|
||||||
|
return keysA.every(key => deepEquals(a[key], b[key]));
|
||||||
|
}
|
|
@ -1,174 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { Language } from "@/data-objects/language.ts";
|
|
||||||
import type { UseQueryReturnType } from "@tanstack/vue-query";
|
|
||||||
import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts";
|
|
||||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
|
||||||
import {computed, nextTick, onMounted, reactive, watch} from "vue";
|
|
||||||
import {getGiftAdapterForType} from "@/views/learning-paths/gift-adapters/gift-adapters.ts";
|
|
||||||
import authService from "@/services/auth/auth-service.ts";
|
|
||||||
import {useCreateSubmissionMutation, useSubmissionsQuery} from "@/queries/submissions.ts";
|
|
||||||
import type {SubmissionDTO} from "@dwengo-1/common/dist/interfaces/submission.d.ts";
|
|
||||||
import type {GroupDTO} from "@dwengo-1/common/interfaces/group";
|
|
||||||
import type {StudentDTO} from "@dwengo-1/common/interfaces/student";
|
|
||||||
import type {LearningObjectIdentifierDTO} from "@dwengo-1/common/interfaces/learning-content";
|
|
||||||
import type {UserProfile} from "oidc-client-ts";
|
|
||||||
|
|
||||||
const isStudent = computed(() => authService.authState.activeRole === "student");
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
hruid: string;
|
|
||||||
language: Language;
|
|
||||||
version: number,
|
|
||||||
group?: {forGroup: number, assignmentNo: number, classId: string}
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const learningObjectHtmlQueryResult: UseQueryReturnType<Document, Error> = useLearningObjectHTMLQuery(
|
|
||||||
() => props.hruid,
|
|
||||||
() => props.language,
|
|
||||||
() => props.version,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentAnswer = reactive(<(string | number | object)[]>[]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
isPending: submissionIsPending,
|
|
||||||
isError: submissionFailed,
|
|
||||||
error: submissionError,
|
|
||||||
isSuccess: submissionSuccess,
|
|
||||||
mutate: submitSolution
|
|
||||||
} = useCreateSubmissionMutation();
|
|
||||||
|
|
||||||
const {
|
|
||||||
isPending: existingSubmissionsIsPending,
|
|
||||||
isError: existingSubmissionsFailed,
|
|
||||||
error: existingSubmissionsError,
|
|
||||||
isSuccess: existingSubmissionsSuccess,
|
|
||||||
data: existingSubmissions
|
|
||||||
} = useSubmissionsQuery(
|
|
||||||
props.hruid,
|
|
||||||
props.language,
|
|
||||||
props.version,
|
|
||||||
props.group?.classId,
|
|
||||||
props.group?.assignmentNo,
|
|
||||||
props.group?.forGroup,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function submitCurrentAnswer(): void {
|
|
||||||
const { forGroup, assignmentNo, classId } = props.group!;
|
|
||||||
const currentUser: UserProfile = authService.authState.user!.profile;
|
|
||||||
const learningObjectIdentifier: LearningObjectIdentifierDTO = {
|
|
||||||
hruid: props.hruid,
|
|
||||||
language: props.language as Language,
|
|
||||||
version: props.version
|
|
||||||
};
|
|
||||||
const submitter: StudentDTO = {
|
|
||||||
id: currentUser.preferred_username!,
|
|
||||||
username: currentUser.preferred_username!,
|
|
||||||
firstName: currentUser.given_name!,
|
|
||||||
lastName: currentUser.family_name!
|
|
||||||
};
|
|
||||||
const group: GroupDTO = {
|
|
||||||
class: classId,
|
|
||||||
assignment: assignmentNo,
|
|
||||||
groupNumber: forGroup
|
|
||||||
}
|
|
||||||
const submission: SubmissionDTO = {
|
|
||||||
learningObjectIdentifier,
|
|
||||||
submitter,
|
|
||||||
group,
|
|
||||||
content: JSON.stringify(currentAnswer)
|
|
||||||
}
|
|
||||||
submitSolution({ data: submission });
|
|
||||||
}
|
|
||||||
|
|
||||||
function forEachQuestion(
|
|
||||||
doAction: (questionIndex: number, questionName: string, questionType: string, questionElement: Element) => void
|
|
||||||
) {
|
|
||||||
const questions = document.querySelectorAll(".gift-question");
|
|
||||||
questions.forEach(question => {
|
|
||||||
const name = question.id.match(/gift-q(\d+)/)?.[1]
|
|
||||||
const questionType = question.className.split(" ")
|
|
||||||
.find(it => it.startsWith("gift-question-type"))
|
|
||||||
?.match(/gift-question-type-([^ ]*)/)?.[1];
|
|
||||||
|
|
||||||
if (!name || isNaN(parseInt(name)) || !questionType) return;
|
|
||||||
|
|
||||||
const index = parseInt(name) - 1;
|
|
||||||
|
|
||||||
doAction(index, name, questionType, question);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function attachQuestionListeners() {
|
|
||||||
forEachQuestion((index, name, type, element) => {
|
|
||||||
getGiftAdapterForType(type)?.installListener(
|
|
||||||
element, (newAnswer) => currentAnswer[index] = newAnswer
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAnswers(answers: (object | string | number)[]) {
|
|
||||||
forEachQuestion((index, name, type, element) => {
|
|
||||||
getGiftAdapterForType(type)?.setAnswer(element, answers[index]);
|
|
||||||
});
|
|
||||||
currentAnswer.splice(0, currentAnswer.length, ...answers);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => nextTick(() => attachQuestionListeners()));
|
|
||||||
|
|
||||||
watch(learningObjectHtmlQueryResult.data, async () => {
|
|
||||||
await nextTick();
|
|
||||||
attachQuestionListeners();
|
|
||||||
setAnswers([1]);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<using-query-result
|
|
||||||
:query-result="learningObjectHtmlQueryResult as UseQueryReturnType<Document, Error>"
|
|
||||||
v-slot="learningPathHtml: { data: Document }"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="learning-object-container"
|
|
||||||
v-html="learningPathHtml.data.body.innerHTML"
|
|
||||||
></div>
|
|
||||||
<p>Last submissions: {{ existingSubmissions }}</p>
|
|
||||||
<p>Your answer: {{ currentAnswer }}</p>
|
|
||||||
<v-btn v-if="isStudent && props.group"
|
|
||||||
prepend-icon="mdi-check"
|
|
||||||
variant="elevated"
|
|
||||||
:loading="submissionIsPending"
|
|
||||||
@click="submitCurrentAnswer()"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</v-btn>
|
|
||||||
</using-query-result>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.learning-object-container {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
:deep(hr) {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
:deep(li) {
|
|
||||||
margin-left: 30px;
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
:deep(img) {
|
|
||||||
max-width: 80%;
|
|
||||||
}
|
|
||||||
:deep(h2),
|
|
||||||
:deep(h3),
|
|
||||||
:deep(h4),
|
|
||||||
:deep(h5),
|
|
||||||
:deep(h6) {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -4,7 +4,7 @@
|
||||||
import { computed, type ComputedRef, ref } from "vue";
|
import { computed, type ComputedRef, ref } from "vue";
|
||||||
import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts";
|
import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import LearningObjectView from "@/views/learning-paths/LearningObjectView.vue";
|
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import LearningPathSearchField from "@/components/LearningPathSearchField.vue";
|
import LearningPathSearchField from "@/components/LearningPathSearchField.vue";
|
||||||
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
||||||
|
@ -185,6 +185,7 @@
|
||||||
<learning-path-search-field></learning-path-search-field>
|
<learning-path-search-field></learning-path-search-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="learning-object-view-container">
|
||||||
<learning-object-view
|
<learning-object-view
|
||||||
:hruid="currentNode.learningobjectHruid"
|
:hruid="currentNode.learningobjectHruid"
|
||||||
:language="currentNode.language"
|
:language="currentNode.language"
|
||||||
|
@ -192,6 +193,7 @@
|
||||||
:group="forGroup"
|
:group="forGroup"
|
||||||
v-if="currentNode"
|
v-if="currentNode"
|
||||||
></learning-object-view>
|
></learning-object-view>
|
||||||
|
</div>
|
||||||
<div class="navigation-buttons-container">
|
<div class="navigation-buttons-container">
|
||||||
<v-btn
|
<v-btn
|
||||||
prepend-icon="mdi-chevron-left"
|
prepend-icon="mdi-chevron-left"
|
||||||
|
@ -227,6 +229,11 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
.learning-object-view-container {
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
.navigation-buttons-container {
|
.navigation-buttons-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -18,9 +18,7 @@ export const multipleChoiceQuestionAdapter: GiftAdapter = {
|
||||||
setAnswer(questionElement: Element, answer: string | number | object): void {
|
setAnswer(questionElement: Element, answer: string | number | object): void {
|
||||||
questionElement.querySelectorAll('input[type=radio]').forEach(element => {
|
questionElement.querySelectorAll('input[type=radio]').forEach(element => {
|
||||||
const input = element as HTMLInputElement;
|
const input = element as HTMLInputElement;
|
||||||
console.log(`input: ${input.value}, answer: ${answer}`);
|
|
||||||
input.checked = String(answer) === String(input.value);
|
input.checked = String(answer) === String(input.value);
|
||||||
console.log(input.checked);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Language } from "@/data-objects/language.ts";
|
||||||
|
import type { UseQueryReturnType } from "@tanstack/vue-query";
|
||||||
|
import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts";
|
||||||
|
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import authService from "@/services/auth/auth-service.ts";
|
||||||
|
import type {SubmissionData} from "@/views/learning-paths/learning-object/submission-data";
|
||||||
|
import LearningObjectContentView from "@/views/learning-paths/learning-object/content/LearningObjectContentView.vue";
|
||||||
|
import LearningObjectSubmissionsView from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue";
|
||||||
|
|
||||||
|
const isStudent = computed(() => authService.authState.activeRole === "student");
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
hruid: string;
|
||||||
|
language: Language;
|
||||||
|
version: number,
|
||||||
|
group?: {forGroup: number, assignmentNo: number, classId: string}
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const learningObjectHtmlQueryResult: UseQueryReturnType<Document, Error> = useLearningObjectHTMLQuery(
|
||||||
|
() => props.hruid,
|
||||||
|
() => props.language,
|
||||||
|
() => props.version,
|
||||||
|
);
|
||||||
|
const currentSubmission = ref<SubmissionData>([]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<using-query-result
|
||||||
|
:query-result="learningObjectHtmlQueryResult as UseQueryReturnType<Document, Error>"
|
||||||
|
v-slot="learningPathHtml: { data: Document }"
|
||||||
|
>
|
||||||
|
<learning-object-content-view
|
||||||
|
:learning-object-content="learningPathHtml.data"
|
||||||
|
v-model:submission-data="currentSubmission"
|
||||||
|
/>
|
||||||
|
<div class="content-submissions-spacer"/>
|
||||||
|
<learning-object-submissions-view
|
||||||
|
v-if="props.group"
|
||||||
|
:group="props.group"
|
||||||
|
:hruid="props.hruid"
|
||||||
|
:language="props.language"
|
||||||
|
:version="props.version"
|
||||||
|
v-model:submission-data="currentSubmission"
|
||||||
|
/>
|
||||||
|
</using-query-result>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(hr) {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
:deep(li) {
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
:deep(img) {
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
:deep(h2),
|
||||||
|
:deep(h3),
|
||||||
|
:deep(h4),
|
||||||
|
:deep(h5),
|
||||||
|
:deep(h6) {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.content-submissions-spacer {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,85 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import type {SubmissionData} from "@/views/learning-paths/learning-object/submission-data";
|
||||||
|
import {getGiftAdapterForType} from "@/views/learning-paths/gift-adapters/gift-adapters.ts";
|
||||||
|
import {computed, nextTick, onMounted, watch} from "vue";
|
||||||
|
import {copyArrayWith} from "@/utils/array-utils.ts";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
learningObjectContent: Document
|
||||||
|
submissionData?: SubmissionData
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:submissionData", value: SubmissionData): void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const submissionData = computed<SubmissionData | undefined>({
|
||||||
|
get: () => props.submissionData,
|
||||||
|
set: (v?: SubmissionData) => v ? emit('update:submissionData', v) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
function forEachQuestion(
|
||||||
|
doAction: (questionIndex: number, questionName: string, questionType: string, questionElement: Element) => void
|
||||||
|
) {
|
||||||
|
const questions = document.querySelectorAll(".gift-question");
|
||||||
|
questions.forEach(question => {
|
||||||
|
const name = question.id.match(/gift-q(\d+)/)?.[1]
|
||||||
|
const questionType = question.className.split(" ")
|
||||||
|
.find(it => it.startsWith("gift-question-type"))
|
||||||
|
?.match(/gift-question-type-([^ ]*)/)?.[1];
|
||||||
|
|
||||||
|
if (!name || isNaN(parseInt(name)) || !questionType) return;
|
||||||
|
|
||||||
|
const index = parseInt(name) - 1;
|
||||||
|
|
||||||
|
doAction(index, name, questionType, question);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachQuestionListeners(): void {
|
||||||
|
let counter = 0;
|
||||||
|
forEachQuestion((index, _name, type, element) => {
|
||||||
|
getGiftAdapterForType(type)?.installListener(
|
||||||
|
element,
|
||||||
|
(newAnswer) => {
|
||||||
|
submissionData.value = copyArrayWith(index, newAnswer, submissionData.value ?? [])
|
||||||
|
}
|
||||||
|
);
|
||||||
|
counter++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAnswers(answers: SubmissionData) {
|
||||||
|
forEachQuestion((index, name, type, element) => {
|
||||||
|
const answer = answers[index];
|
||||||
|
if (answer !== null && answer !== undefined) {
|
||||||
|
getGiftAdapterForType(type)?.setAnswer(element, answer);
|
||||||
|
} else if (answer === undefined) {
|
||||||
|
answers[index] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
submissionData.value = answers;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => nextTick(() => attachQuestionListeners()));
|
||||||
|
|
||||||
|
watch(() => props.learningObjectContent, async () => {
|
||||||
|
await nextTick();
|
||||||
|
attachQuestionListeners();
|
||||||
|
});
|
||||||
|
watch(() => props.submissionData, async () => {
|
||||||
|
await nextTick();
|
||||||
|
setAnswers(props.submissionData ?? []);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="learning-object-container"
|
||||||
|
v-html="learningObjectContent.body.innerHTML"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
1
frontend/src/views/learning-paths/learning-object/submission-data.d.ts
vendored
Normal file
1
frontend/src/views/learning-paths/learning-object/submission-data.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export type SubmissionData = (string | number | object | null)[];
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {SubmissionData} from "@/views/learning-paths/learning-object/submission-data";
|
||||||
|
import type {SubmissionDTO} from "@dwengo-1/common/interfaces/submission";
|
||||||
|
import {Language} from "@/data-objects/language.ts";
|
||||||
|
import {useSubmissionsQuery} from "@/queries/submissions.ts";
|
||||||
|
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||||
|
import SubmitButton from "@/views/learning-paths/learning-object/submissions/SubmitButton.vue";
|
||||||
|
import {watch} from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
submissionData?: SubmissionData,
|
||||||
|
hruid: string;
|
||||||
|
language: Language;
|
||||||
|
version: number,
|
||||||
|
group: {forGroup: number, assignmentNo: number, classId: string}
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:submissionData", value: SubmissionData): void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const submissionQuery = useSubmissionsQuery(
|
||||||
|
() => props.hruid,
|
||||||
|
() => props.language,
|
||||||
|
() => props.version,
|
||||||
|
() => props.group.classId,
|
||||||
|
() => props.group.assignmentNo,
|
||||||
|
() => props.group.forGroup,
|
||||||
|
() => true
|
||||||
|
);
|
||||||
|
|
||||||
|
function loadSubmission(submission: SubmissionDTO) {
|
||||||
|
emit("update:submissionData", JSON.parse(submission.content));
|
||||||
|
console.log(`emitted: ${JSON.parse(submission.content)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(submissionQuery.data, () => {
|
||||||
|
const submissions = submissionQuery.data.value;
|
||||||
|
if (submissions && submissions.length > 0) {
|
||||||
|
loadSubmission(submissions[submissions.length - 1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<using-query-result :query-result="submissionQuery" v-slot="submissions: { data: SubmissionDTO[] }">
|
||||||
|
<submit-button
|
||||||
|
:hruid="props.hruid"
|
||||||
|
:language="props.language"
|
||||||
|
:version="props.version"
|
||||||
|
:group="props.group"
|
||||||
|
:submission-data="props.submissionData"
|
||||||
|
:submissions="submissions.data"
|
||||||
|
/>
|
||||||
|
</using-query-result>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
|
@ -0,0 +1,102 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed} from "vue";
|
||||||
|
import authService from "@/services/auth/auth-service.ts";
|
||||||
|
import type {SubmissionData} from "@/views/learning-paths/learning-object/submission-data";
|
||||||
|
import {Language} from "@/data-objects/language.ts";
|
||||||
|
import type {SubmissionDTO} from "@dwengo-1/common/interfaces/submission";
|
||||||
|
import {useCreateSubmissionMutation} from "@/queries/submissions.ts";
|
||||||
|
import {deepEquals} from "@/utils/deep-equals.ts";
|
||||||
|
import type {UserProfile} from "oidc-client-ts";
|
||||||
|
import type {LearningObjectIdentifierDTO} from "@dwengo-1/common/interfaces/learning-content";
|
||||||
|
import type {StudentDTO} from "@dwengo-1/common/interfaces/student";
|
||||||
|
import type {GroupDTO} from "@dwengo-1/common/interfaces/group";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
submissionData?: SubmissionData,
|
||||||
|
submissions: SubmissionDTO[],
|
||||||
|
hruid: string;
|
||||||
|
language: Language;
|
||||||
|
version: number,
|
||||||
|
group: {forGroup: number, assignmentNo: number, classId: string}
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isPending: submissionIsPending,
|
||||||
|
isError: submissionFailed,
|
||||||
|
error: submissionError,
|
||||||
|
isSuccess: submissionSuccess,
|
||||||
|
mutate: submitSolution
|
||||||
|
} = useCreateSubmissionMutation();
|
||||||
|
|
||||||
|
const isStudent = computed(() => authService.authState.activeRole === "student");
|
||||||
|
|
||||||
|
const isSubmitDisabled = computed(() => {
|
||||||
|
if (!props.submissionData || props.submissions === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (props.submissionData.some(answer => answer === null)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (props.submissions.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return deepEquals(
|
||||||
|
JSON.parse(props.submissions[props.submissions.length - 1].content),
|
||||||
|
props.submissionData
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function submitCurrentAnswer(): void {
|
||||||
|
const { forGroup, assignmentNo, classId } = props.group!;
|
||||||
|
const currentUser: UserProfile = authService.authState.user!.profile;
|
||||||
|
const learningObjectIdentifier: LearningObjectIdentifierDTO = {
|
||||||
|
hruid: props.hruid,
|
||||||
|
language: props.language as Language,
|
||||||
|
version: props.version
|
||||||
|
};
|
||||||
|
const submitter: StudentDTO = {
|
||||||
|
id: currentUser.preferred_username!,
|
||||||
|
username: currentUser.preferred_username!,
|
||||||
|
firstName: currentUser.given_name!,
|
||||||
|
lastName: currentUser.family_name!
|
||||||
|
};
|
||||||
|
const group: GroupDTO = {
|
||||||
|
class: classId,
|
||||||
|
assignment: assignmentNo,
|
||||||
|
groupNumber: forGroup
|
||||||
|
}
|
||||||
|
const submission: SubmissionDTO = {
|
||||||
|
learningObjectIdentifier,
|
||||||
|
submitter,
|
||||||
|
group,
|
||||||
|
content: JSON.stringify(props.submissionData)
|
||||||
|
}
|
||||||
|
submitSolution({ data: submission });
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonText = computed(() => {
|
||||||
|
if (props.submissionData && props.submissionData.length === 0) {
|
||||||
|
return t("markAsDone");
|
||||||
|
}
|
||||||
|
return t("submit");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-btn v-if="isStudent"
|
||||||
|
prepend-icon="mdi-check"
|
||||||
|
variant="elevated"
|
||||||
|
:loading="submissionIsPending"
|
||||||
|
:disabled="isSubmitDisabled"
|
||||||
|
@click="submitCurrentAnswer()"
|
||||||
|
>
|
||||||
|
{{ buttonText }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
Loading…
Add table
Add a link
Reference in a new issue