feat(backend): opvragen van leerobjecten van een leerkracht
This commit is contained in:
		
							parent
							
								
									78353d6b65
								
							
						
					
					
						commit
						6600441b08
					
				
					 11 changed files with 152 additions and 38 deletions
				
			
		|  | @ -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<void> { | ||||
|     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<void> { | ||||
|  | @ -74,9 +82,13 @@ export async function getAttachment(req: Request, res: Response): Promise<void> | |||
|     res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); | ||||
| } | ||||
| 
 | ||||
| export async function handlePostLearningObject(req: Request, res: Response): Promise<void> { | ||||
|     if (!req.files || !req.files[0]) { | ||||
| export async function handlePostLearningObject(req: AuthenticatedRequest, res: Response): Promise<void> { | ||||
|     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); | ||||
| } | ||||
|  |  | |||
|  | @ -33,9 +33,9 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj | |||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> { | ||||
|     public async findAllByAdmin(adminUsername: string): Promise<LearningObject[]> { | ||||
|         return this.find( | ||||
|             { admins: teacher }, | ||||
|             { admins: { $contains: adminUsername } }, | ||||
|             { populate: ['admins'] } // Make sure to load admin relations
 | ||||
|         ); | ||||
|     } | ||||
|  |  | |||
|  | @ -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<Teacher> = new Collection<Teacher>(this); | ||||
| 
 | ||||
|     @Property({ type: 'string' }) | ||||
|     title!: string; | ||||
|  | @ -84,7 +94,7 @@ export class LearningObject { | |||
|         entity: () => Attachment, | ||||
|         mappedBy: 'learningObject', | ||||
|     }) | ||||
|     attachments: Attachment[] = []; | ||||
|     attachments: Collection<Attachment> = new Collection<Attachment>(this); | ||||
| 
 | ||||
|     @Property({ type: 'blob' }) | ||||
|     content!: Buffer; | ||||
|  |  | |||
|  | @ -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<FilteredLearningObject[]> { | ||||
|         const learningObjectRepo = getLearningObjectRepository(); | ||||
|         const learningObjects = await learningObjectRepo.findAllByAdmin(adminUsername); | ||||
|         return learningObjects | ||||
|                 .map(it => convertLearningObject(it)) | ||||
|                 .filter(it => it != null); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export default databaseLearningObjectProvider; | ||||
|  |  | |||
|  | @ -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<FilteredLearningObject[]> { | ||||
|         return []; // The dwengo database does not contain any learning objects administrated by users.
 | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export default dwengoApiLearningObjectProvider; | ||||
|  |  | |||
|  | @ -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<string | null>; | ||||
| 
 | ||||
|     /** | ||||
|      * Obtain all learning object who have the user with the given username as an admin. | ||||
|      */ | ||||
|     getLearningObjectsAdministratedBy(username: string): Promise<FilteredLearningObject[]>; | ||||
| } | ||||
|  |  | |||
|  | @ -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<FilteredLearningObject[]> { | ||||
|         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<void> { | ||||
|     async storeLearningObject(learningObjectPath: string, admins: string[]): Promise<LearningObject> { | ||||
|         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; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<Learni | |||
| 
 | ||||
|     const zip = await unzipper.Open.file(filePath); | ||||
| 
 | ||||
|     let metadata: LearningObjectMetadata | null = null; | ||||
|     let metadata: LearningObjectMetadata | undefined = undefined; | ||||
|     const attachments: {name: string, content: Buffer}[] = []; | ||||
|     let content: Buffer | null = null; | ||||
|     let content: Buffer | undefined = undefined; | ||||
| 
 | ||||
|     if (zip.files.length == 0) { | ||||
|         throw new BadRequestException("empty_zip") | ||||
|     } | ||||
| 
 | ||||
|     for (const file of zip.files) { | ||||
|         if (file.type === "Directory") { | ||||
|             throw new BadRequestException("The learning object zip file should not contain directories."); | ||||
|         } else if (file.path === "metadata.json") { | ||||
|             metadata = await processMetadataJson(file); | ||||
|         } else if (file.path.startsWith("index.")) { | ||||
|             content = await processFile(file); | ||||
|         } else { | ||||
|             attachments.push({ | ||||
|                 name: file.path, | ||||
|                 content: await processFile(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.json file"); | ||||
|         throw new BadRequestException("missing_metadata"); | ||||
|     } | ||||
|     if (!content) { | ||||
|         throw new BadRequestException("Missing index file"); | ||||
|         throw new BadRequestException("missing_index"); | ||||
|     } | ||||
| 
 | ||||
|     const learningObject = learningObjectRepo.create(metadata); | ||||
| 
 | ||||
|     const learningObject = learningObjectRepo.create({ | ||||
|         admins: [], | ||||
|         available: metadata.available ?? true, | ||||
|         content: content, | ||||
|         contentType: metadata.content_type, | ||||
|         copyright: metadata.copyright, | ||||
|         description: metadata.description, | ||||
|         educationalGoals: metadata.educational_goals, | ||||
|         hruid: metadata.hruid, | ||||
|         keywords: metadata.keywords, | ||||
|         language: metadata.language, | ||||
|         license: "", | ||||
|         returnValue: metadata.return_value, | ||||
|         skosConcepts: metadata.skos_concepts, | ||||
|         teacherExclusive: metadata.teacher_exclusive, | ||||
|         title: metadata.title, | ||||
|         version: metadata.version | ||||
|     }); | ||||
|     const attachmentEntities = attachments.map(it => 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; | ||||
| } | ||||
|  |  | |||
|  | @ -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 []; | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger