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 { mapToGroupDTO } from './group.js'; | ||||
| import { mapToGroupDTOId } from './group.js'; | ||||
| import { mapToStudentDTO } from './student.js'; | ||||
| import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; | ||||
| import { getSubmissionRepository } from '../data/repositories'; | ||||
|  | @ -13,11 +13,10 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { | |||
|             language: submission.learningObjectLanguage, | ||||
|             version: submission.learningObjectVersion, | ||||
|         }, | ||||
| 
 | ||||
|         submissionNumber: submission.submissionNumber, | ||||
|         submitter: mapToStudentDTO(submission.submitter), | ||||
|         time: submission.submissionTime, | ||||
|         group: mapToGroupDTO(submission.onBehalfOf), | ||||
|         group: mapToGroupDTOId(submission.onBehalfOf), | ||||
|         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})`); | ||||
|         res.status(err.status).json(err); | ||||
|     } 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); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,13 +1,13 @@ | |||
| { | ||||
|     "welcome": "Willkommen", | ||||
|     "student": "schüler", | ||||
|     "teacher": "lehrer", | ||||
|     "student": "Schüler", | ||||
|     "teacher": "Lehrer", | ||||
|     "assignments": "Aufgaben", | ||||
|     "classes": "Klasses", | ||||
|     "classes": "Klassen", | ||||
|     "discussions": "Diskussionen", | ||||
|     "login": "einloggen", | ||||
|     "logout": "ausloggen", | ||||
|     "cancel": "kündigen", | ||||
|     "cancel": "abbrechen", | ||||
|     "logoutVerification": "Sind Sie sicher, dass Sie sich abmelden wollen?", | ||||
|     "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.", | ||||
|  | @ -23,10 +23,10 @@ | |||
|     "submitCode": "senden", | ||||
|     "members": "Mitglieder", | ||||
|     "themes": "Themen", | ||||
|     "choose-theme": "Wähle ein thema", | ||||
|     "choose-theme": "Wählen Sie ein Thema", | ||||
|     "choose-age": "Alter auswählen", | ||||
|     "theme-options": { | ||||
|         "all": "Alle themen", | ||||
|         "all": "Alle Themen", | ||||
|         "culture": "Kultur", | ||||
|         "electricity-and-mechanics": "Elektrizität und Mechanik", | ||||
|         "nature-and-climate": "Natur und Klima", | ||||
|  | @ -37,11 +37,11 @@ | |||
|         "algorithms": "Algorithmisches Denken" | ||||
|     }, | ||||
|     "age-options": { | ||||
|         "all": "Alle altersgruppen", | ||||
|         "all": "Alle Altersgruppen", | ||||
|         "primary-school": "Grundschule", | ||||
|         "lower-secondary": "12-14 jahre alt", | ||||
|         "upper-secondary": "14-16 jahre alt", | ||||
|         "high-school": "16-18 jahre alt", | ||||
|         "lower-secondary": "12-14 Jahre alt", | ||||
|         "upper-secondary": "14-16 Jahre alt", | ||||
|         "high-school": "16-18 Jahre alt", | ||||
|         "older": "18 und älter" | ||||
|     }, | ||||
|     "read-more": "Mehr lesen", | ||||
|  | @ -73,7 +73,9 @@ | |||
|     "accept": "akzeptieren", | ||||
|     "deny": "ablehnen", | ||||
|     "sent": "sent", | ||||
|     "failed": "gescheitert", | ||||
|     "failed": "fehlgeschlagen", | ||||
|     "wrong": "etwas ist schief gelaufen", | ||||
|     "created": "erstellt" | ||||
|     "created": "erstellt", | ||||
|     "submit": "Einreichen", | ||||
|     "markAsDone": "Als fertig markieren" | ||||
| } | ||||
|  |  | |||
|  | @ -75,5 +75,7 @@ | |||
|     "sent": "sent", | ||||
|     "failed": "failed", | ||||
|     "wrong": "something went wrong", | ||||
|     "created": "created" | ||||
|     "created": "created", | ||||
|     "submit": "Submit", | ||||
|     "markAsDone": "Mark as done" | ||||
| } | ||||
|  |  | |||
|  | @ -75,5 +75,7 @@ | |||
|     "sent": "verzonden", | ||||
|     "failed": "mislukt", | ||||
|     "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( | ||||
|     hruid: MaybeRefOrGetter<string>, | ||||
|     language: MaybeRefOrGetter<Language>, | ||||
|     forGroup?: MaybeRefOrGetter<{forGroup: number, assignmentNo: number, classId: string}>, | ||||
|     forGroup?: MaybeRefOrGetter<{forGroup: number, assignmentNo: number, classId: string} | undefined>, | ||||
| ): UseQueryReturnType<LearningPath, Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: [LEARNING_PATH_KEY, "get", toValue(hruid), toValue(language), toValue(forGroup)], | ||||
|         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)]; | ||||
|             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 { | ||||
|     QueryClient, | ||||
|  | @ -13,17 +13,7 @@ import {LEARNING_PATH_KEY} from "@/queries/learning-paths.ts"; | |||
| import {LEARNING_OBJECT_KEY} from "@/queries/learning-objects.ts"; | ||||
| import type {Language} from "@dwengo-1/common/util/language"; | ||||
| 
 | ||||
| function submissionsQueryKey( | ||||
|     hruid: string, | ||||
|     language: Language, | ||||
|     version: number, | ||||
|     classid: string, | ||||
|     assignmentNumber: number, | ||||
|     groupNumber?: number, | ||||
|     full?: boolean | ||||
| ) { | ||||
|     return ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber, full ?? false]; | ||||
| } | ||||
| export const SUBMISSION_KEY = "submissions"; | ||||
| 
 | ||||
| function submissionQueryKey( | ||||
|     hruid: string, | ||||
|  | @ -72,19 +62,8 @@ export async function invalidateAllSubmissionKeys( | |||
|     }); | ||||
| } | ||||
| 
 | ||||
| function checkEnabled( | ||||
|     classid: string | undefined, | ||||
|     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 checkEnabled(properties: MaybeRefOrGetter<unknown>[]): boolean { | ||||
|     return properties.every(prop => !!toValue(prop)); | ||||
| } | ||||
| 
 | ||||
| function toValues( | ||||
|  | @ -110,7 +89,10 @@ export function useSubmissionsQuery( | |||
|     assignmentNumber: MaybeRefOrGetter<number | undefined>, | ||||
|     groupNumber: MaybeRefOrGetter<number | undefined>, | ||||
|     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 languageVal = toValue(language); | ||||
|             const versionVal = toValue(version); | ||||
|  | @ -119,22 +101,12 @@ export function useSubmissionsQuery( | |||
|             const groupNumberVal = toValue(groupNumber); | ||||
|             const fullVal = toValue(full); | ||||
| 
 | ||||
|     return useQuery({ | ||||
|         queryKey: computed(() => | ||||
|             submissionsQueryKey( | ||||
|                 hruidVal!, | ||||
|                 languageVal!, | ||||
|                 versionVal!, | ||||
|                 classIdVal!, | ||||
|                 assignmentNumberVal!, | ||||
|                 groupNumberVal, | ||||
|                 fullVal | ||||
|             ) | ||||
|         ), | ||||
|         queryFn: async () => new SubmissionController(hruidVal!).getAll( | ||||
|             const response = await new SubmissionController(hruidVal!).getAll( | ||||
|                 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>, | ||||
|     submissionNumber: MaybeRefOrGetter<number | undefined>, | ||||
| ): UseQueryReturnType<SubmissionResponse, Error> { | ||||
|     const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, submissionNumber, true); | ||||
| 
 | ||||
|     const hruidVal = toValue(hruid); | ||||
|     const languageVal = toValue(language); | ||||
|     const versionVal = toValue(version); | ||||
|  | @ -192,11 +162,6 @@ export function useCreateSubmissionMutation(): UseMutationReturnType< | |||
|                 const {hruid, language, version} = response.submission.learningObjectIdentifier; | ||||
|                 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({ | ||||
|  | @ -216,7 +181,7 @@ export function useDeleteSubmissionMutation(): UseMutationReturnType< | |||
|     const queryClient = useQueryClient(); | ||||
| 
 | ||||
|     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) => { | ||||
|             if (!response.submission.group) { | ||||
|                 await invalidateAllSubmissionKeys(queryClient); | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue"; | |||
| import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue"; | ||||
| import UserHomePage from "@/views/homepage/UserHomePage.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({ | ||||
|     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 type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; | ||||
|     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 LearningPathSearchField from "@/components/LearningPathSearchField.vue"; | ||||
|     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||
|  | @ -185,6 +185,7 @@ | |||
|                 <learning-path-search-field></learning-path-search-field> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="learning-object-view-container"> | ||||
|             <learning-object-view | ||||
|                 :hruid="currentNode.learningobjectHruid" | ||||
|                 :language="currentNode.language" | ||||
|  | @ -192,6 +193,7 @@ | |||
|                 :group="forGroup" | ||||
|                 v-if="currentNode" | ||||
|             ></learning-object-view> | ||||
|         </div> | ||||
|         <div class="navigation-buttons-container"> | ||||
|             <v-btn | ||||
|                 prepend-icon="mdi-chevron-left" | ||||
|  | @ -227,6 +229,11 @@ | |||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|     } | ||||
|     .learning-object-view-container { | ||||
|         padding-left: 20px; | ||||
|         padding-right: 20px; | ||||
|         padding-bottom: 20px; | ||||
|     } | ||||
|     .navigation-buttons-container { | ||||
|         padding: 20px; | ||||
|         display: flex; | ||||
|  |  | |||
|  | @ -18,9 +18,7 @@ export const multipleChoiceQuestionAdapter: GiftAdapter = { | |||
|     setAnswer(questionElement: Element, answer: string | number | object): void { | ||||
|         questionElement.querySelectorAll('input[type=radio]').forEach(element => { | ||||
|             const input = element as HTMLInputElement; | ||||
|             console.log(`input: ${input.value}, answer: ${answer}`); | ||||
|             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> | ||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger