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 { envVars, getEnvVar } from '../util/envVars.js'; | ||||||
| import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| import {UploadedFile} from "express-fileupload"; | import {UploadedFile} from "express-fileupload"; | ||||||
|  | import {AuthenticatedRequest} from "../middleware/auth/authenticated-request"; | ||||||
| 
 | 
 | ||||||
| function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO { | function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO { | ||||||
|     if (!req.params.hruid) { |     if (!req.params.hruid) { | ||||||
|  | @ -31,6 +32,12 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAllLearningObjects(req: Request, res: Response): Promise<void> { | export async function getAllLearningObjects(req: Request, res: Response): Promise<void> { | ||||||
|  |     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); | ||||||
|  | 
 | ||||||
|  |         res.json(learningObjects); | ||||||
|  |     } else { // Else he/she wants all learning objects on the path specified by the request parameters.
 | ||||||
|         const learningPathId = getLearningPathIdentifierFromRequest(req); |         const learningPathId = getLearningPathIdentifierFromRequest(req); | ||||||
|         const full = req.query.full; |         const full = req.query.full; | ||||||
| 
 | 
 | ||||||
|  | @ -43,6 +50,7 @@ export async function getAllLearningObjects(req: Request, res: Response): Promis | ||||||
| 
 | 
 | ||||||
|         res.json({ learningObjects: learningObjects }); |         res.json({ learningObjects: learningObjects }); | ||||||
|     } |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| export async function getLearningObject(req: Request, res: Response): Promise<void> { | export async function getLearningObject(req: Request, res: Response): Promise<void> { | ||||||
|     const learningObjectId = getLearningObjectIdentifierFromRequest(req); |     const learningObjectId = getLearningObjectIdentifierFromRequest(req); | ||||||
|  | @ -74,9 +82,13 @@ export async function getAttachment(req: Request, res: Response): Promise<void> | ||||||
|     res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); |     res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function handlePostLearningObject(req: Request, res: Response): Promise<void> { | export async function handlePostLearningObject(req: AuthenticatedRequest, res: Response): Promise<void> { | ||||||
|     if (!req.files || !req.files[0]) { |     if (!req.files || !req.files.learningObject) { | ||||||
|         throw new BadRequestException('No file uploaded'); |         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( |         return this.find( | ||||||
|             { admins: teacher }, |             { admins: { $contains: adminUsername } }, | ||||||
|             { populate: ['admins'] } // Make sure to load admin relations
 |             { 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 { Attachment } from './attachment.entity.js'; | ||||||
| import { Teacher } from '../users/teacher.entity.js'; | import { Teacher } from '../users/teacher.entity.js'; | ||||||
| import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; | import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; | ||||||
|  | @ -28,7 +38,7 @@ export class LearningObject { | ||||||
|     @ManyToMany({ |     @ManyToMany({ | ||||||
|         entity: () => Teacher, |         entity: () => Teacher, | ||||||
|     }) |     }) | ||||||
|     admins!: Teacher[]; |     admins: Collection<Teacher> = new Collection<Teacher>(this); | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'string' }) |     @Property({ type: 'string' }) | ||||||
|     title!: string; |     title!: string; | ||||||
|  | @ -84,7 +94,7 @@ export class LearningObject { | ||||||
|         entity: () => Attachment, |         entity: () => Attachment, | ||||||
|         mappedBy: 'learningObject', |         mappedBy: 'learningObject', | ||||||
|     }) |     }) | ||||||
|     attachments: Attachment[] = []; |     attachments: Collection<Attachment> = new Collection<Attachment>(this); | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'blob' }) |     @Property({ type: 'blob' }) | ||||||
|     content!: Buffer; |     content!: Buffer; | ||||||
|  |  | ||||||
|  | @ -109,6 +109,17 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | ||||||
|         ); |         ); | ||||||
|         return learningObjects.filter((it) => it !== null); |         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; | export default databaseLearningObjectProvider; | ||||||
|  |  | ||||||
|  | @ -135,6 +135,13 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | ||||||
| 
 | 
 | ||||||
|         return html; |         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; | 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). |      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). | ||||||
|      */ |      */ | ||||||
|     getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null>; |     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, |     LearningObjectIdentifierDTO, | ||||||
|     LearningPathIdentifier |     LearningPathIdentifier | ||||||
| } from '@dwengo-1/common/interfaces/learning-content'; | } 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 {processLearningObjectZip} from "./learning-object-zip-processing-service"; | ||||||
| import {BadRequestException} from "../../exceptions/bad-request-exception"; | import {BadRequestException} from "../../exceptions/bad-request-exception"; | ||||||
|  | import {LearningObject} from "../../entities/content/learning-object.entity"; | ||||||
| 
 | 
 | ||||||
| function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { | function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { | ||||||
|     if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { |     if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { | ||||||
|  | @ -50,19 +51,40 @@ const learningObjectService = { | ||||||
|         return getProvider(id).getLearningObjectHTML(id); |         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. |      * 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 learningObjectRepository = getLearningObjectRepository(); | ||||||
|         const learningObject = await processLearningObjectZip(learningObjectPath); |         const learningObject = await processLearningObjectZip(learningObjectPath); | ||||||
| 
 | 
 | ||||||
|  |         console.log(learningObject); | ||||||
|         if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { |         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}); |         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 {BadRequestException} from "../../exceptions/bad-request-exception"; | ||||||
| import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; | 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. |  * Process an uploaded zip file and construct a LearningObject from its contents. | ||||||
|  * @param filePath Path of the zip file to process. |  * @param filePath Path of the zip file to process. | ||||||
|  | @ -15,16 +18,19 @@ export async function processLearningObjectZip(filePath: string): Promise<Learni | ||||||
| 
 | 
 | ||||||
|     const zip = await unzipper.Open.file(filePath); |     const zip = await unzipper.Open.file(filePath); | ||||||
| 
 | 
 | ||||||
|     let metadata: LearningObjectMetadata | null = null; |     let metadata: LearningObjectMetadata | undefined = undefined; | ||||||
|     const attachments: {name: string, content: Buffer}[] = []; |     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) { |     for (const file of zip.files) { | ||||||
|         if (file.type === "Directory") { |         if (file.type !== "Directory") { | ||||||
|             throw new BadRequestException("The learning object zip file should not contain directories."); |             if (METADATA_PATH_REGEX.test(file.path)) { | ||||||
|         } else if (file.path === "metadata.json") { |  | ||||||
|                 metadata = await processMetadataJson(file); |                 metadata = await processMetadataJson(file); | ||||||
|         } else if (file.path.startsWith("index.")) { |             } else if (CONTENT_PATH_REGEX.test(file.path)) { | ||||||
|                 content = await processFile(file); |                 content = await processFile(file); | ||||||
|             } else { |             } else { | ||||||
|                 attachments.push({ |                 attachments.push({ | ||||||
|  | @ -33,22 +39,41 @@ export async function processLearningObjectZip(filePath: string): Promise<Learni | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     if (!metadata) { |     if (!metadata) { | ||||||
|         throw new BadRequestException("Missing metadata.json file"); |         throw new BadRequestException("missing_metadata"); | ||||||
|     } |     } | ||||||
|     if (!content) { |     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({ |     const attachmentEntities = attachments.map(it => attachmentRepo.create({ | ||||||
|         name: it.name, |         name: it.name, | ||||||
|         content: it.content, |         content: it.content, | ||||||
|         mimeType: mime.lookup(it.name) || "text/plain", |         mimeType: mime.lookup(it.name) || "text/plain", | ||||||
|         learningObject |         learningObject | ||||||
|     })) |     })) | ||||||
|     learningObject.attachments.push(...attachmentEntities); |     attachmentEntities.forEach(it => learningObject.attachments.add(it)); | ||||||
| 
 | 
 | ||||||
|     return learningObject; |     return learningObject; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -124,7 +124,7 @@ export async function getTeacherQuestions(username: string, full: boolean): Prom | ||||||
| 
 | 
 | ||||||
|     // Find all learning objects that this teacher manages
 |     // Find all learning objects that this teacher manages
 | ||||||
|     const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository(); |     const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository(); | ||||||
|     const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher); |     const learningObjects: LearningObject[] = await learningObjectRepository.findAllByAdmin(teacher); | ||||||
| 
 | 
 | ||||||
|     if (!learningObjects || learningObjects.length === 0) { |     if (!learningObjects || learningObjects.length === 0) { | ||||||
|         return []; |         return []; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  | 
 | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  | 
 | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger