diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index d11833dc..1dd7c9e0 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -2,7 +2,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { Language } from '@dwengo-1/common/util/language'; -import { Teacher } from '../../entities/users/teacher.entity.js'; export class LearningObjectRepository extends DwengoEntityRepository { public async findByIdentifier(identifier: LearningObjectIdentifier): Promise { @@ -35,7 +34,11 @@ export class LearningObjectRepository extends DwengoEntityRepository { return this.find( - { admins: { $contains: adminUsername } }, + { + admins: { + username: adminUsername + } + }, { populate: ['admins'] } // Make sure to load admin relations ); } diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 9d6c6673..0a229fde 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -9,7 +9,6 @@ import { } from '@dwengo-1/common/interfaces/learning-content'; import {getLearningObjectRepository, getTeacherRepository} from "../../data/repositories"; import {processLearningObjectZip} from "./learning-object-zip-processing-service"; -import {BadRequestException} from "../../exceptions/bad-request-exception"; import {LearningObject} from "../../entities/content/learning-object.entity"; function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { @@ -67,7 +66,6 @@ const learningObjectService = { const learningObjectRepository = getLearningObjectRepository(); const learningObject = await processLearningObjectZip(learningObjectPath); - console.log(learningObject); if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid; } @@ -75,10 +73,10 @@ const learningObjectService = { // Lookup the admin teachers based on their usernames and add them to the admins of the learning object. const teacherRepo = getTeacherRepository(); const adminTeachers = await Promise.all( - admins.map(it => teacherRepo.findByUsername(it)) + admins.map(async it => teacherRepo.findByUsername(it)) ); adminTeachers.forEach(it => { - if (it != null) { + if (it !== null) { learningObject.admins.add(it); } }); diff --git a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts index 53c4ca9c..213c3f17 100644 --- a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts +++ b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts @@ -4,6 +4,7 @@ import {LearningObject} from "../../entities/content/learning-object.entity"; import {getAttachmentRepository, getLearningObjectRepository} from "../../data/repositories"; import {BadRequestException} from "../../exceptions/bad-request-exception"; import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; +import { ReturnValue } from '../../entities/content/return-value.entity'; const METADATA_PATH_REGEX = /.*[/^]metadata\.json$/; const CONTENT_PATH_REGEX = /.*[/^]content\.[a-zA-Z]*$/; @@ -13,33 +14,38 @@ const CONTENT_PATH_REGEX = /.*[/^]content\.[a-zA-Z]*$/; * @param filePath Path of the zip file to process. */ export async function processLearningObjectZip(filePath: string): Promise { - const learningObjectRepo = getLearningObjectRepository(); - const attachmentRepo = getAttachmentRepository(); + let zip: unzipper.CentralDirectory; + try { + zip = await unzipper.Open.file(filePath); + } catch(_: unknown) { + throw new BadRequestException("invalid_zip"); + } - const zip = await unzipper.Open.file(filePath); let metadata: LearningObjectMetadata | undefined = undefined; const attachments: {name: string, content: Buffer}[] = []; let content: Buffer | undefined = undefined; - if (zip.files.length == 0) { + if (zip.files.length === 0) { throw new BadRequestException("empty_zip") } - for (const file of zip.files) { - if (file.type !== "Directory") { - if (METADATA_PATH_REGEX.test(file.path)) { - metadata = await processMetadataJson(file); - } else if (CONTENT_PATH_REGEX.test(file.path)) { - content = await processFile(file); - } else { - attachments.push({ - name: file.path, - content: await processFile(file) - }); + await Promise.all( + zip.files.map(async file => { + if (file.type !== "Directory") { + if (METADATA_PATH_REGEX.test(file.path)) { + metadata = await processMetadataJson(file); + } else if (CONTENT_PATH_REGEX.test(file.path)) { + content = await processFile(file); + } else { + attachments.push({ + name: file.path, + content: await processFile(file) + }); + } } - } - } + }) + ); if (!metadata) { throw new BadRequestException("missing_metadata"); @@ -49,20 +55,30 @@ export async function processLearningObjectZip(filePath: string): Promise learningObject.attachments.add(it)); - + })); + attachmentEntities.forEach(it => { learningObject.attachments.add(it); }); return learningObject; } diff --git a/frontend/src/controllers/base-controller.ts b/frontend/src/controllers/base-controller.ts index 64f2363d..bb450618 100644 --- a/frontend/src/controllers/base-controller.ts +++ b/frontend/src/controllers/base-controller.ts @@ -37,6 +37,28 @@ export abstract class BaseController { return response.data; } + /** + * Sends a POST-request with a form-data body with the given file. + * + * @param path Relative path in the api to send the request to. + * @param formFieldName The name of the form field in which the file should be. + * @param file The file to upload. + * @param queryParams The query parameters. + * @returns The response the POST request generated. + */ + protected async postFile(path: string, formFieldName: string, file: File, queryParams?: QueryParams): Promise { + const formData = new FormData(); + formData.append(formFieldName, file); + const response = await apiClient.post(this.absolutePathFor(path), formData, { + params: queryParams, + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + BaseController.assertSuccessResponse(response) + return response.data; + } + protected async delete(path: string, queryParams?: QueryParams): Promise { const response = await apiClient.delete(this.absolutePathFor(path), { params: queryParams }); BaseController.assertSuccessResponse(response); diff --git a/frontend/src/controllers/learning-objects.ts b/frontend/src/controllers/learning-objects.ts index d62ba1f4..7239b88f 100644 --- a/frontend/src/controllers/learning-objects.ts +++ b/frontend/src/controllers/learning-objects.ts @@ -14,4 +14,12 @@ export class LearningObjectController extends BaseController { async getHTML(hruid: string, language: Language, version: number): Promise { return this.get(`/${hruid}/html`, { language, version }, "document"); } + + async getAllAdministratedBy(admin: string): Promise { + return this.get("/", { admin }); + } + + async upload(learningObjectZip: File): Promise { + return this.postFile("/", "learningObject", learningObjectZip); + } } diff --git a/frontend/src/queries/learning-objects.ts b/frontend/src/queries/learning-objects.ts index 35ed7ae4..5e7612e9 100644 --- a/frontend/src/queries/learning-objects.ts +++ b/frontend/src/queries/learning-objects.ts @@ -1,9 +1,10 @@ import { type MaybeRefOrGetter, toValue } from "vue"; import type { Language } from "@/data-objects/language.ts"; -import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { getLearningObjectController } from "@/controllers/controllers.ts"; import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; +import type { AxiosError } from "axios"; export const LEARNING_OBJECT_KEY = "learningObject"; const learningObjectController = getLearningObjectController(); @@ -24,15 +25,15 @@ export function useLearningObjectMetadataQuery( } export function useLearningObjectHTMLQuery( - hruid: MaybeRefOrGetter, - language: MaybeRefOrGetter, - version: MaybeRefOrGetter, + hruid: MaybeRefOrGetter, + language: MaybeRefOrGetter, + version: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ queryKey: [LEARNING_OBJECT_KEY, "html", hruid, language, version], queryFn: async () => { const [hruidVal, languageVal, versionVal] = [toValue(hruid), toValue(language), toValue(version)]; - return learningObjectController.getHTML(hruidVal, languageVal, versionVal); + return learningObjectController.getHTML(hruidVal!, languageVal!, versionVal!); }, enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)) && Boolean(toValue(version)), }); @@ -55,3 +56,25 @@ export function useLearningObjectListForPathQuery( enabled: () => Boolean(toValue(learningPath)), }); } + +export function useLearningObjectListForAdminQuery( + admin: MaybeRefOrGetter +): UseQueryReturnType { + return useQuery({ + queryKey: [LEARNING_OBJECT_KEY, "forAdmin", admin], + queryFn: async () => { + const adminVal = toValue(admin); + return await learningObjectController.getAllAdministratedBy(adminVal!); + }, + enabled: () => toValue(admin) !== undefined + }); +} + +export function useUploadLearningObjectMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ learningObjectZip }) => await learningObjectController.upload(learningObjectZip), + onSuccess: async () => { await queryClient.invalidateQueries({queryKey: [LEARNING_OBJECT_KEY, "forAdmin"]}); } + }); +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 359eab1a..c8a1ebc4 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -14,6 +14,7 @@ import UserHomePage from "@/views/homepage/UserHomePage.vue"; import SingleTheme from "@/views/SingleTheme.vue"; import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; import authService from "@/services/auth/auth-service"; +import OwnLearningContentPage from "@/views/own-learning-content/OwnLearningContentPage.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -114,6 +115,12 @@ const router = createRouter({ component: LearningPathSearchPage, meta: { requiresAuth: true }, }, + { + path: "my", + name: "OwnLearningContentPage", + component: OwnLearningContentPage, + meta: { requiresAuth: true } + }, { path: ":hruid/:language/:learningObjectHruid", name: "LearningPath", diff --git a/frontend/src/views/own-learning-content/LearningObjectUploadButton.vue b/frontend/src/views/own-learning-content/LearningObjectUploadButton.vue new file mode 100644 index 00000000..34211467 --- /dev/null +++ b/frontend/src/views/own-learning-content/LearningObjectUploadButton.vue @@ -0,0 +1,76 @@ + + + diff --git a/frontend/src/views/own-learning-content/OwnLearningContentPage.vue b/frontend/src/views/own-learning-content/OwnLearningContentPage.vue index 85af7153..609c8433 100644 --- a/frontend/src/views/own-learning-content/OwnLearningContentPage.vue +++ b/frontend/src/views/own-learning-content/OwnLearningContentPage.vue @@ -1,11 +1,52 @@ diff --git a/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue b/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue index 85af7153..831f1cf0 100644 --- a/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue +++ b/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue @@ -1,11 +1,80 @@ diff --git a/frontend/src/views/own-learning-content/OwnLearningPathsView.vue b/frontend/src/views/own-learning-content/OwnLearningPathsView.vue new file mode 100644 index 00000000..b199202e --- /dev/null +++ b/frontend/src/views/own-learning-content/OwnLearningPathsView.vue @@ -0,0 +1,9 @@ + + + + +