diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index 3622912c..ba8fdef3 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -8,6 +8,7 @@ import { NotFoundException } from '../exceptions/not-found-exception.js'; import { envVars, getEnvVar } from '../util/envVars.js'; import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import {UploadedFile} from "express-fileupload"; +import {AuthenticatedRequest} from "../middleware/auth/authenticated-request"; function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO { if (!req.params.hruid) { @@ -31,17 +32,24 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif } export async function getAllLearningObjects(req: Request, res: Response): Promise { - const learningPathId = getLearningPathIdentifierFromRequest(req); - const full = req.query.full; + if (req.query.admin) { // If the admin query parameter is present, the user wants to have all learning objects with this admin. + const learningObjects = + await learningObjectService.getLearningObjectsAdministratedBy(req.query.admin as string); - let learningObjects: FilteredLearningObject[] | string[]; - if (full) { - learningObjects = await learningObjectService.getLearningObjectsFromPath(learningPathId); - } else { - learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); + res.json(learningObjects); + } else { // Else he/she wants all learning objects on the path specified by the request parameters. + const learningPathId = getLearningPathIdentifierFromRequest(req); + const full = req.query.full; + + let learningObjects: FilteredLearningObject[] | string[]; + if (full) { + learningObjects = await learningObjectService.getLearningObjectsFromPath(learningPathId); + } else { + learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); + } + + res.json({ learningObjects: learningObjects }); } - - res.json({ learningObjects: learningObjects }); } export async function getLearningObject(req: Request, res: Response): Promise { @@ -74,9 +82,13 @@ export async function getAttachment(req: Request, res: Response): Promise res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); } -export async function handlePostLearningObject(req: Request, res: Response): Promise { - if (!req.files || !req.files[0]) { +export async function handlePostLearningObject(req: AuthenticatedRequest, res: Response): Promise { + if (!req.files || !req.files.learningObject) { throw new BadRequestException('No file uploaded'); } - await learningObjectService.storeLearningObject((req.files[0] as UploadedFile).tempFilePath); + const learningObject = await learningObjectService.storeLearningObject( + (req.files.learningObject as UploadedFile).tempFilePath, + [req.auth!.username] + ); + res.json(learningObject); } diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index 889a1594..d11833dc 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -33,9 +33,9 @@ export class LearningObjectRepository extends DwengoEntityRepository { + public async findAllByAdmin(adminUsername: string): Promise { return this.find( - { admins: teacher }, + { admins: { $contains: adminUsername } }, { populate: ['admins'] } // Make sure to load admin relations ); } diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index e0ae09d6..825bf744 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -1,4 +1,14 @@ -import { ArrayType, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; +import { + ArrayType, + Collection, + Embedded, + Entity, + Enum, + ManyToMany, + OneToMany, + PrimaryKey, + Property +} from '@mikro-orm/core'; import { Attachment } from './attachment.entity.js'; import { Teacher } from '../users/teacher.entity.js'; import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; @@ -28,7 +38,7 @@ export class LearningObject { @ManyToMany({ entity: () => Teacher, }) - admins!: Teacher[]; + admins: Collection = new Collection(this); @Property({ type: 'string' }) title!: string; @@ -84,7 +94,7 @@ export class LearningObject { entity: () => Attachment, mappedBy: 'learningObject', }) - attachments: Attachment[] = []; + attachments: Collection = new Collection(this); @Property({ type: 'blob' }) content!: Buffer; diff --git a/backend/src/services/learning-objects/database-learning-object-provider.ts b/backend/src/services/learning-objects/database-learning-object-provider.ts index 0b805a56..9d16d820 100644 --- a/backend/src/services/learning-objects/database-learning-object-provider.ts +++ b/backend/src/services/learning-objects/database-learning-object-provider.ts @@ -109,6 +109,17 @@ const databaseLearningObjectProvider: LearningObjectProvider = { ); return learningObjects.filter((it) => it !== null); }, + + /** + * Returns all learning objects containing the given username as an admin. + */ + async getLearningObjectsAdministratedBy(adminUsername: string): Promise { + const learningObjectRepo = getLearningObjectRepository(); + const learningObjects = await learningObjectRepo.findAllByAdmin(adminUsername); + return learningObjects + .map(it => convertLearningObject(it)) + .filter(it => it != null); + } }; export default databaseLearningObjectProvider; diff --git a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts index 4a4bdc54..804d2d20 100644 --- a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts +++ b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts @@ -135,6 +135,13 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { return html; }, + + /** + * Obtain all learning objects who have the user with the given username as an admin. + */ + async getLearningObjectsAdministratedBy(_adminUsername: string): Promise { + return []; // The dwengo database does not contain any learning objects administrated by users. + } }; export default dwengoApiLearningObjectProvider; diff --git a/backend/src/services/learning-objects/learning-object-provider.ts b/backend/src/services/learning-objects/learning-object-provider.ts index 14848bc0..69ad268d 100644 --- a/backend/src/services/learning-objects/learning-object-provider.ts +++ b/backend/src/services/learning-objects/learning-object-provider.ts @@ -20,4 +20,9 @@ export interface LearningObjectProvider { * Obtain a HTML-rendering of the learning object with the given identifier (as a string). */ getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise; + + /** + * Obtain all learning object who have the user with the given username as an admin. + */ + getLearningObjectsAdministratedBy(username: string): Promise; } diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 9a0912ae..9d6c6673 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -7,9 +7,10 @@ import { LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; -import {getLearningObjectRepository} from "../../data/repositories"; +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 { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { @@ -50,19 +51,40 @@ const learningObjectService = { return getProvider(id).getLearningObjectHTML(id); }, + /** + * Obtain all learning objects administrated by the user with the given username. + */ + async getLearningObjectsAdministratedBy(adminUsername: string): Promise { + return databaseLearningObjectProvider.getLearningObjectsAdministratedBy(adminUsername); + }, /** * Store the learning object in the given zip file in the database. + * @param learningObjectPath The path where the uploaded learning object resides. + * @param admins The usernames of the users which should be administrators of the learning object. */ - async storeLearningObject(learningObjectPath: string): Promise { + async storeLearningObject(learningObjectPath: string, admins: string[]): Promise { const learningObjectRepository = getLearningObjectRepository(); const learningObject = await processLearningObjectZip(learningObjectPath); + console.log(learningObject); if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { - throw new BadRequestException("Learning object name must start with the user content prefix!"); + learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid; } + // 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)) + ); + adminTeachers.forEach(it => { + if (it != null) { + learningObject.admins.add(it); + } + }); + await learningObjectRepository.save(learningObject, {preventOverwrite: true}); + return learningObject; } }; 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 fcf80e2b..53c4ca9c 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 @@ -5,6 +5,9 @@ import {getAttachmentRepository, getLearningObjectRepository} from "../../data/r import {BadRequestException} from "../../exceptions/bad-request-exception"; import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; +const METADATA_PATH_REGEX = /.*[/^]metadata\.json$/; +const CONTENT_PATH_REGEX = /.*[/^]content\.[a-zA-Z]*$/; + /** * Process an uploaded zip file and construct a LearningObject from its contents. * @param filePath Path of the zip file to process. @@ -15,40 +18,62 @@ export async function processLearningObjectZip(filePath: string): Promise attachmentRepo.create({ name: it.name, content: it.content, mimeType: mime.lookup(it.name) || "text/plain", learningObject })) - learningObject.attachments.push(...attachmentEntities); + attachmentEntities.forEach(it => learningObject.attachments.add(it)); return learningObject; } diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index 4fdb15be..8f41df8b 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -124,7 +124,7 @@ export async function getTeacherQuestions(username: string, full: boolean): Prom // Find all learning objects that this teacher manages const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository(); - const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher); + const learningObjects: LearningObject[] = await learningObjectRepository.findAllByAdmin(teacher); if (!learningObjects || learningObjects.length === 0) { return []; diff --git a/frontend/src/views/own-learning-content/OwnLearningContentPage.vue b/frontend/src/views/own-learning-content/OwnLearningContentPage.vue new file mode 100644 index 00000000..85af7153 --- /dev/null +++ b/frontend/src/views/own-learning-content/OwnLearningContentPage.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue b/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue new file mode 100644 index 00000000..85af7153 --- /dev/null +++ b/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue @@ -0,0 +1,11 @@ + + + + +