feat(frontend): LearningObjectService en LearningPathService geïmplementeerd.
This commit is contained in:
		
							parent
							
								
									8b0fc4263f
								
							
						
					
					
						commit
						3c3fddb7d0
					
				
					 24 changed files with 375 additions and 84 deletions
				
			
		
							
								
								
									
										39
									
								
								frontend/src/components/UsingRemoteResource.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								frontend/src/components/UsingRemoteResource.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | ||||||
|  | <script setup lang="ts" generic="T"> | ||||||
|  |     import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
|  |     import {computed} from "vue"; | ||||||
|  |     import {useI18n} from "vue-i18n"; | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         resource: RemoteResource<T> | ||||||
|  |     }>() | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     const isLoading = computed(() => props.resource.state.type === 'loading'); | ||||||
|  |     const isError = computed(() => props.resource.state.type === 'error'); | ||||||
|  | 
 | ||||||
|  |     // `data` will be correctly inferred as `T` | ||||||
|  |     const data = computed(() => props.resource.data); | ||||||
|  | 
 | ||||||
|  |     const error = computed(() => props.resource.state.type === "error" ? props.resource.state : null); | ||||||
|  |     const errorMessage = computed(() => | ||||||
|  |         error.value?.message ? error.value.message : JSON.stringify(error.value?.error) | ||||||
|  |     ); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <div v-if="isLoading"> | ||||||
|  |         <v-progress-circular indeterminate></v-progress-circular> | ||||||
|  |     </div> | ||||||
|  |     <div v-if="isError"> | ||||||
|  |         <v-empty-state | ||||||
|  |             icon="mdi-alert-circle-outline" | ||||||
|  |             text="errorMessage" | ||||||
|  |             :title=t("error_title") | ||||||
|  |         ></v-empty-state> | ||||||
|  |     </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
| { | { | ||||||
|     "welcome": "Willkommen" |     "welcome": "Willkommen", | ||||||
|  |     "error_title": "Fehler" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,5 +5,6 @@ | ||||||
|     "assignments": "assignments", |     "assignments": "assignments", | ||||||
|     "classes": "classes", |     "classes": "classes", | ||||||
|     "discussions": "discussions", |     "discussions": "discussions", | ||||||
|     "logout": "log out" |     "logout": "log out", | ||||||
|  |     "error_title": "Error" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
| { | { | ||||||
|     "welcome": "Bienvenue" |     "welcome": "Bienvenue", | ||||||
|  |     "error_title": "Erreur" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,5 +5,6 @@ | ||||||
|     "assignments": "opdrachten", |     "assignments": "opdrachten", | ||||||
|     "classes": "klassen", |     "classes": "klassen", | ||||||
|     "discussions": "discussies", |     "discussions": "discussies", | ||||||
|     "logout": "log uit" |     "logout": "log uit", | ||||||
|  |     "error_title": "Fout" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import i18n from "./i18n/i18n.ts"; | ||||||
| // Components
 | // Components
 | ||||||
| import App from "./App.vue"; | import App from "./App.vue"; | ||||||
| import router from "./router"; | import router from "./router"; | ||||||
|  | import {aliases, mdi} from "vuetify/iconsets/mdi"; | ||||||
| 
 | 
 | ||||||
| const app = createApp(App); | const app = createApp(App); | ||||||
| 
 | 
 | ||||||
|  | @ -23,6 +24,13 @@ document.head.appendChild(link); | ||||||
| const vuetify = createVuetify({ | const vuetify = createVuetify({ | ||||||
|     components, |     components, | ||||||
|     directives, |     directives, | ||||||
|  |     icons: { | ||||||
|  |         defaultSet: "mdi", | ||||||
|  |         aliases, | ||||||
|  |         sets: { | ||||||
|  |             mdi | ||||||
|  |         } | ||||||
|  |     } | ||||||
| }); | }); | ||||||
| app.use(vuetify); | app.use(vuetify); | ||||||
| app.use(i18n); | app.use(i18n); | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								frontend/src/services/api-client/api-exceptions.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/services/api-client/api-exceptions.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import type {AxiosResponse} from "axios"; | ||||||
|  | 
 | ||||||
|  | export class HttpErrorStatusException extends Error { | ||||||
|  |     public readonly statusCode: number; | ||||||
|  | 
 | ||||||
|  |     constructor(response: AxiosResponse<any, any>) { | ||||||
|  |         super(`${response.statusText} (${response.status})`); | ||||||
|  |         this.statusCode = response.status; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | import {type Params, RestEndpoint} from "@/services/api-client/endpoints/rest-endpoint.ts"; | ||||||
|  | import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
|  | 
 | ||||||
|  | export class DeleteEndpoint<PP extends Params, QP extends Params, R> extends RestEndpoint<PP, QP, undefined, R> { | ||||||
|  |     readonly method = "GET"; | ||||||
|  | 
 | ||||||
|  |     public delete(pathParams: PP, queryParams: QP): RemoteResource<R> { | ||||||
|  |         return super.request(pathParams, queryParams, undefined); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								frontend/src/services/api-client/endpoints/get-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/services/api-client/endpoints/get-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import {type Params, RestEndpoint} from "@/services/api-client/endpoints/rest-endpoint.ts"; | ||||||
|  | import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
|  | 
 | ||||||
|  | export class GetEndpoint<PP extends Params, QP extends Params, R> extends RestEndpoint<PP, QP, undefined, R> { | ||||||
|  |     readonly method = "GET"; | ||||||
|  | 
 | ||||||
|  |     public get(pathParams: PP, queryParams: QP): RemoteResource<R> { | ||||||
|  |         return super.request(pathParams, queryParams, undefined); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								frontend/src/services/api-client/endpoints/post-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/services/api-client/endpoints/post-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import {type Params, RestEndpoint} from "@/services/api-client/endpoints/rest-endpoint.ts"; | ||||||
|  | import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
|  | 
 | ||||||
|  | export class PostEndpoint<PP extends Params, QP extends Params, B, R> extends RestEndpoint<PP, QP, B, R> { | ||||||
|  |     readonly method = "POST"; | ||||||
|  | 
 | ||||||
|  |     public post(pathParams: PP, queryParams: QP, body: B): RemoteResource<R> { | ||||||
|  |         return super.request(pathParams, queryParams, body); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								frontend/src/services/api-client/endpoints/rest-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/src/services/api-client/endpoints/rest-endpoint.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | import {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
|  | import apiClient from "@/services/api-client/api-client.ts"; | ||||||
|  | import {HttpErrorStatusException} from "@/services/api-client/api-exceptions"; | ||||||
|  | 
 | ||||||
|  | export abstract class RestEndpoint<PP extends Params, QP extends Params, B, R> { | ||||||
|  |     public abstract readonly method: "GET" | "POST" | "PUT" | "DELETE"; | ||||||
|  |     constructor(public readonly url: string) { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected request(pathParams: PP, queryParams: QP, body: B): RemoteResource<R> { | ||||||
|  |         let urlFilledIn = this.url; | ||||||
|  |         urlFilledIn.replace(/:(\w+)([/$])/g, (_, key, after) => | ||||||
|  |             (key in pathParams ? encodeURIComponent(pathParams[key]) : `:${key}`) + after | ||||||
|  |         ); | ||||||
|  |         return new RemoteResource(async () => { | ||||||
|  |             const response = await apiClient.request<R>({ | ||||||
|  |                 url: urlFilledIn, | ||||||
|  |                 method: this.method, | ||||||
|  |                 params: queryParams, | ||||||
|  |                 data: body, | ||||||
|  |             }); | ||||||
|  |             if (response.status / 100 !== 2) { | ||||||
|  |                 throw new HttpErrorStatusException(response); | ||||||
|  |             } | ||||||
|  |             return response.data; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type Params = {[key: string]: string | number | boolean}; | ||||||
							
								
								
									
										70
									
								
								frontend/src/services/api-client/remote-resource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								frontend/src/services/api-client/remote-resource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | ||||||
|  | export class RemoteResource<T> { | ||||||
|  |     static NOT_LOADED: NotLoadedState = {type: "notLoaded"}; | ||||||
|  |     static LOADING: LoadingState = {type: "loading"}; | ||||||
|  | 
 | ||||||
|  |     private state: NotLoadedState | LoadingState | ErrorState | SuccessState<T> = RemoteResource.NOT_LOADED; | ||||||
|  | 
 | ||||||
|  |     constructor(private readonly requestFn: () => Promise<T>) { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async request(): Promise<T | undefined> { | ||||||
|  |         this.state = RemoteResource.LOADING; | ||||||
|  |         try { | ||||||
|  |             let resource = await this.requestFn(); | ||||||
|  |             this.state = { | ||||||
|  |                 type: "success", | ||||||
|  |                 data: resource | ||||||
|  |             }; | ||||||
|  |             return resource; | ||||||
|  |         } catch (e: any) { | ||||||
|  |             this.state = { | ||||||
|  |                 type: "error", | ||||||
|  |                 errorCode: e.statusCode, | ||||||
|  |                 message: e.message, | ||||||
|  |                 error: e | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public startRequestInBackground(): RemoteResource<T> { | ||||||
|  |         this.request().then(); | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public get data(): T | undefined { | ||||||
|  |         if (this.state.type === "success") { | ||||||
|  |             return this.state.data; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public map<U>(mappingFn: (content: T) => U): RemoteResource<U> { | ||||||
|  |         return new RemoteResource<U>(async () => { | ||||||
|  |             await this.request(); | ||||||
|  |             if (this.state.type === "success") { | ||||||
|  |                 return mappingFn(this.state.data); | ||||||
|  |             } else if (this.state.type === "error") { | ||||||
|  |                 throw this.state.error; | ||||||
|  |             } else { | ||||||
|  |                 throw new Error("Fetched resource, but afterwards, it was neither in a success nor in an error state. " + | ||||||
|  |                     "This should never happen."); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type NotLoadedState = { | ||||||
|  |     type: "notLoaded" | ||||||
|  | }; | ||||||
|  | type LoadingState = { | ||||||
|  |     type: "loading" | ||||||
|  | }; | ||||||
|  | type ErrorState = { | ||||||
|  |     type: "error", | ||||||
|  |     errorCode?: number, | ||||||
|  |     message?: string, | ||||||
|  |     error: any | ||||||
|  | }; | ||||||
|  | type SuccessState<T> = { | ||||||
|  |     type: "success", | ||||||
|  |     data: T | ||||||
|  | } | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import apiClient from "@/services/api-client.ts"; | import apiClient from "@/services/api-client/api-client.ts"; | ||||||
| import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts"; | import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import { User, UserManager } from "oidc-client-ts"; | ||||||
| import { loadAuthConfig } from "@/services/auth/auth-config-loader.ts"; | import { loadAuthConfig } from "@/services/auth/auth-config-loader.ts"; | ||||||
| import authStorage from "./auth-storage.ts"; | import authStorage from "./auth-storage.ts"; | ||||||
| import { loginRoute } from "@/config.ts"; | import { loginRoute } from "@/config.ts"; | ||||||
| import apiClient from "@/services/api-client.ts"; | import apiClient from "@/services/api-client/api-client.ts"; | ||||||
| import router from "@/router"; | import router from "@/router"; | ||||||
| import type { AxiosError } from "axios"; | import type { AxiosError } from "axios"; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | import {GetEndpoint} from "@/services/api-client/endpoints/get-endpoint.ts"; | ||||||
|  | import type {LearningObject} from "@/services/learning-content/learning-object.ts"; | ||||||
|  | import type {Language} from "@/services/learning-content/language.ts"; | ||||||
|  | import type {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
|  | 
 | ||||||
|  | const getLearningObjectMetadataEndpoint = new GetEndpoint<{hruid: string}, {language: Language, version: number}, LearningObject>( | ||||||
|  |     "/learningObject/:hruid" | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export function getLearningObjectMetadata(hruid: string, language: Language, version: number): RemoteResource<LearningObject> { | ||||||
|  |     return getLearningObjectMetadataEndpoint | ||||||
|  |         .get({hruid}, {language, version}); | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								frontend/src/services/learning-content/learning-object.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/src/services/learning-content/learning-object.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | import type {Language} from "@/services/learning-content/language.ts"; | ||||||
|  | 
 | ||||||
|  | export interface EducationalGoal { | ||||||
|  |     source: string; | ||||||
|  |     id: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface ReturnValue { | ||||||
|  |     callback_url: string; | ||||||
|  |     callback_schema: Record<string, any>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface LearningObject { | ||||||
|  |     key: string; | ||||||
|  |     _id: string; | ||||||
|  |     uuid: string; | ||||||
|  |     version: number; | ||||||
|  |     title: string; | ||||||
|  |     htmlUrl: string; | ||||||
|  |     language: Language; | ||||||
|  |     difficulty: number; | ||||||
|  |     estimatedTime?: number; | ||||||
|  |     available: boolean; | ||||||
|  |     teacherExclusive: boolean; | ||||||
|  |     educationalGoals: EducationalGoal[]; | ||||||
|  |     keywords: string[]; | ||||||
|  |     description: string; | ||||||
|  |     targetAges: number[]; | ||||||
|  |     contentType: string; | ||||||
|  |     contentLocation?: string; | ||||||
|  |     skosConcepts?: string[]; | ||||||
|  |     returnValue?: ReturnValue; | ||||||
|  | } | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | import {GetEndpoint} from "@/services/api-client/endpoints/get-endpoint.ts"; | ||||||
|  | import {LearningPath, type LearningPathDTO} from "@/services/learning-content/learning-path.ts"; | ||||||
|  | import type {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
|  | 
 | ||||||
|  | const searchLearningPathsEndpoint = new GetEndpoint<{}, {query: string}, LearningPathDTO[]>( | ||||||
|  |     "/learningObjects/:query" | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export function searchLearningPaths(query: string): RemoteResource<LearningPath[]> { | ||||||
|  |     return searchLearningPathsEndpoint | ||||||
|  |         .get({}, {query: query}) | ||||||
|  |         .map(dtos => dtos.map(dto => LearningPath.fromDTO(dto))); | ||||||
|  | } | ||||||
							
								
								
									
										118
									
								
								frontend/src/services/learning-content/learning-path.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								frontend/src/services/learning-content/learning-path.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,118 @@ | ||||||
|  | import type {Language} from "@/services/learning-content/language.ts"; | ||||||
|  | import type {RemoteResource} from "@/services/api-client/remote-resource.ts"; | ||||||
|  | import type {LearningObject} from "@/services/learning-content/learning-object.ts"; | ||||||
|  | import {getLearningObjectMetadata} from "@/services/learning-content/learning-object-service.ts"; | ||||||
|  | 
 | ||||||
|  | export interface LearningPathDTO { | ||||||
|  |     language: string; | ||||||
|  |     hruid: string; | ||||||
|  |     title: string; | ||||||
|  |     description: string; | ||||||
|  |     image?: string; // Image might be missing, so it's optional
 | ||||||
|  |     num_nodes: number; | ||||||
|  |     num_nodes_left: number; | ||||||
|  |     nodes: LearningPathNodeDTO[]; | ||||||
|  |     keywords: string; | ||||||
|  |     target_ages: number[]; | ||||||
|  |     min_age: number; | ||||||
|  |     max_age: number; | ||||||
|  |     __order: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface LearningPathNodeDTO { | ||||||
|  |     _id: string; | ||||||
|  |     learningobject_hruid: string; | ||||||
|  |     version: number; | ||||||
|  |     language: Language; | ||||||
|  |     start_node?: boolean; | ||||||
|  |     transitions: LearningPathTransitionDTO[]; | ||||||
|  |     created_at: string; | ||||||
|  |     updatedAt: string; | ||||||
|  |     done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized.
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface LearningPathTransitionDTO { | ||||||
|  |     default: boolean; | ||||||
|  |     _id: string; | ||||||
|  |     next: { | ||||||
|  |         _id: string; | ||||||
|  |         hruid: string; | ||||||
|  |         version: number; | ||||||
|  |         language: string; | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class LearningPathNode { | ||||||
|  |     public learningObject: RemoteResource<LearningObject> | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         public readonly learningobjectHruid: string, | ||||||
|  |         public readonly version: number, | ||||||
|  |         public readonly language: Language, | ||||||
|  |         public readonly transitions: {next: LearningPathNode, default: boolean}[], | ||||||
|  |         public readonly createdAt: Date, | ||||||
|  |         public readonly updatedAt: Date | ||||||
|  |     ) { | ||||||
|  |         this.learningObject = getLearningObjectMetadata(learningobjectHruid, language, version); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static fromDTOAndOtherNodes(dto: LearningPathNodeDTO, otherNodes: LearningPathNodeDTO[]): LearningPathNode { | ||||||
|  |         return new LearningPathNode( | ||||||
|  |             dto.learningobject_hruid, | ||||||
|  |             dto.version, | ||||||
|  |             dto.language, | ||||||
|  |             dto.transitions.map(transDto => { | ||||||
|  |                 let nextNodeDto = otherNodes.filter(it => | ||||||
|  |                     it.learningobject_hruid === transDto.next.hruid | ||||||
|  |                     && it.language === transDto.next.language | ||||||
|  |                     && it.version === transDto.next.version | ||||||
|  |                 ); | ||||||
|  |                 if (nextNodeDto.length !== 1) { | ||||||
|  |                     throw new Error(`Invalid learning path! There is a transition to node` | ||||||
|  |                         + `${transDto.next.hruid}/${transDto.next.language}/${transDto.next.version}, but there are` | ||||||
|  |                         + `${nextNodeDto.length} such nodes.`); | ||||||
|  |                 } | ||||||
|  |                 return { | ||||||
|  |                     next: LearningPathNode.fromDTOAndOtherNodes(nextNodeDto[0], otherNodes), | ||||||
|  |                     default: transDto.default | ||||||
|  |                 } | ||||||
|  |             }), | ||||||
|  |             new Date(dto.created_at), | ||||||
|  |             new Date(dto.updatedAt), | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class LearningPath { | ||||||
|  |     constructor( | ||||||
|  |         public readonly language: string, | ||||||
|  |         public readonly hruid: string, | ||||||
|  |         public readonly title: string, | ||||||
|  |         public readonly description: string, | ||||||
|  |         public readonly amountOfNodes: number, | ||||||
|  |         public readonly amountOfNodesLeft: number, | ||||||
|  |         public readonly keywords: string[], | ||||||
|  |         public readonly targetAges: {min: number; max: number}, | ||||||
|  |         public readonly startNode: LearningPathNode, | ||||||
|  |         public readonly image?: string // Image might be missing, so it's optional
 | ||||||
|  |     ) { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static fromDTO(dto: LearningPathDTO): LearningPath { | ||||||
|  |         let startNodeDto = dto.nodes.filter(it => it.start_node); | ||||||
|  |         if (startNodeDto.length !== 1) { | ||||||
|  |             throw new Error(`Invalid learning path! Expected precisely one start node, but there were ${startNodeDto.length}.`); | ||||||
|  |         } | ||||||
|  |         return new LearningPath( | ||||||
|  |             dto.language, | ||||||
|  |             dto.hruid, | ||||||
|  |             dto.title, | ||||||
|  |             dto.description, | ||||||
|  |             dto.num_nodes, | ||||||
|  |             dto.num_nodes_left, | ||||||
|  |             dto.keywords.split(' '), | ||||||
|  |             {min: dto.min_age, max: dto.max_age}, | ||||||
|  |             LearningPathNode.fromDTOAndOtherNodes(startNodeDto[0], dto.nodes) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,37 +0,0 @@ | ||||||
| import type {Language} from "@/services/learning-paths/language.ts"; |  | ||||||
| 
 |  | ||||||
| export interface LearningPathIdentifier { |  | ||||||
|     hruid: string; |  | ||||||
|     language: Language; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface EducationalGoal { |  | ||||||
|     source: string; |  | ||||||
|     id: string; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface ReturnValue { |  | ||||||
|     callback_url: string; |  | ||||||
|     callback_schema: Record<string, any>; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface LearningObjectMetadata { |  | ||||||
|     _id: string; |  | ||||||
|     uuid: string; |  | ||||||
|     hruid: string; |  | ||||||
|     version: number; |  | ||||||
|     language: Language; |  | ||||||
|     title: string; |  | ||||||
|     description: string; |  | ||||||
|     difficulty: number; |  | ||||||
|     estimated_time: number; |  | ||||||
|     available: boolean; |  | ||||||
|     teacher_exclusive: boolean; |  | ||||||
|     educational_goals: EducationalGoal[]; |  | ||||||
|     keywords: string[]; |  | ||||||
|     target_ages: number[]; |  | ||||||
|     content_type: string; // Markdown, image, etc.
 |  | ||||||
|     content_location?: string; |  | ||||||
|     skos_concepts?: string[]; |  | ||||||
|     return_value?: ReturnValue; |  | ||||||
| } |  | ||||||
|  | @ -1,40 +0,0 @@ | ||||||
| import type {Language} from "@/services/learning-paths/language.ts"; |  | ||||||
| 
 |  | ||||||
| export interface LearningPath { |  | ||||||
|     language: string; |  | ||||||
|     hruid: string; |  | ||||||
|     title: string; |  | ||||||
|     description: string; |  | ||||||
|     image?: string; // Image might be missing, so it's optional
 |  | ||||||
|     num_nodes: number; |  | ||||||
|     num_nodes_left: number; |  | ||||||
|     nodes: LearningObjectNode[]; |  | ||||||
|     keywords: string; |  | ||||||
|     target_ages: number[]; |  | ||||||
|     min_age: number; |  | ||||||
|     max_age: number; |  | ||||||
|     __order: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface LearningObjectNode { |  | ||||||
|     _id: string; |  | ||||||
|     learningobject_hruid: string; |  | ||||||
|     version: number; |  | ||||||
|     language: Language; |  | ||||||
|     start_node?: boolean; |  | ||||||
|     transitions: Transition[]; |  | ||||||
|     created_at: string; |  | ||||||
|     updatedAt: string; |  | ||||||
|     done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized.
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface Transition { |  | ||||||
|     default: boolean; |  | ||||||
|     _id: string; |  | ||||||
|     next: { |  | ||||||
|         _id: string; |  | ||||||
|         hruid: string; |  | ||||||
|         version: number; |  | ||||||
|         language: string; |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import auth from "@/services/auth/auth-service.ts"; |     import auth from "@/services/auth/auth-service.ts"; | ||||||
|     import apiClient from "@/services/api-client.ts"; |     import apiClient from "@/services/api-client/api-client.ts"; | ||||||
|     import { ref } from "vue"; |     import { ref } from "vue"; | ||||||
| 
 | 
 | ||||||
|     const testResponse = ref(null); |     const testResponse = ref(null); | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger