feat(backend): PUSH, PUT en DELETE endpoints voor leerpaden aangemaakt.
This commit is contained in:
		
							parent
							
								
									20c04370b5
								
							
						
					
					
						commit
						30ca3b70de
					
				
					 8 changed files with 186 additions and 44 deletions
				
			
		|  | @ -7,51 +7,89 @@ import { BadRequestException } from '../exceptions/bad-request-exception.js'; | |||
| import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||
| import { Group } from '../entities/assignments/group.entity.js'; | ||||
| import { getAssignmentRepository, getGroupRepository } from '../data/repositories.js'; | ||||
| import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js'; | ||||
| import { LearningPath, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| import { getTeacher } from '../services/teachers.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Fetch learning paths based on query parameters. | ||||
|  */ | ||||
| export async function getLearningPaths(req: Request, res: Response): Promise<void> { | ||||
|     const hruids = req.query.hruid; | ||||
|     const themeKey = req.query.theme as string; | ||||
|     const searchQuery = req.query.search as string; | ||||
|     const language = (req.query.language as string) || FALLBACK_LANG; | ||||
| 
 | ||||
|     const forGroupNo = req.query.forGroup as string; | ||||
|     const assignmentNo = req.query.assignmentNo as string; | ||||
|     const classId = req.query.classId as string; | ||||
| 
 | ||||
|     let forGroup: Group | undefined; | ||||
| 
 | ||||
|     if (forGroupNo) { | ||||
|         if (!assignmentNo || !classId) { | ||||
|             throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.'); | ||||
|         } | ||||
|         const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo)); | ||||
|         if (assignment) { | ||||
|             forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     let hruidList; | ||||
| 
 | ||||
|     if (hruids) { | ||||
|         hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)]; | ||||
|     } else if (themeKey) { | ||||
|         const theme = themes.find((t) => t.title === themeKey); | ||||
|         if (theme) { | ||||
|             hruidList = theme.hruids; | ||||
|         } else { | ||||
|             throw new NotFoundException(`Theme "${themeKey}" not found.`); | ||||
|         } | ||||
|     } else if (searchQuery) { | ||||
|         const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup); | ||||
|         res.json(searchResults); | ||||
|         return; | ||||
|     const admin = req.query.admin; | ||||
|     if (admin) { | ||||
|         const paths = await learningPathService.getLearningPathsAdministratedBy(admin as string); | ||||
|         res.json(paths); | ||||
|     } else { | ||||
|         hruidList = themes.flatMap((theme) => theme.hruids); | ||||
|     } | ||||
|         const hruids = req.query.hruid; | ||||
|         const themeKey = req.query.theme as string; | ||||
|         const searchQuery = req.query.search as string; | ||||
|         const language = (req.query.language as string) || FALLBACK_LANG; | ||||
| 
 | ||||
|     const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup); | ||||
|     res.json(learningPaths.data); | ||||
|         const forGroupNo = req.query.forGroup as string; | ||||
|         const assignmentNo = req.query.assignmentNo as string; | ||||
|         const classId = req.query.classId as string; | ||||
| 
 | ||||
|         let forGroup: Group | undefined; | ||||
| 
 | ||||
|         if (forGroupNo) { | ||||
|             if (!assignmentNo || !classId) { | ||||
|                 throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.'); | ||||
|             } | ||||
|             const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo)); | ||||
|             if (assignment) { | ||||
|                 forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let hruidList; | ||||
| 
 | ||||
|         if (hruids) { | ||||
|             hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)]; | ||||
|         } else if (themeKey) { | ||||
|             const theme = themes.find((t) => t.title === themeKey); | ||||
|             if (theme) { | ||||
|                 hruidList = theme.hruids; | ||||
|             } else { | ||||
|                 throw new NotFoundException(`Theme "${themeKey}" not found.`); | ||||
|             } | ||||
|         } else if (searchQuery) { | ||||
|             const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup); | ||||
|             res.json(searchResults); | ||||
|             return; | ||||
|         } else { | ||||
|             hruidList = themes.flatMap((theme) => theme.hruids); | ||||
|         } | ||||
| 
 | ||||
|         const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup); | ||||
|         res.json(learningPaths.data); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function postOrPutLearningPath(isPut: boolean): (req: AuthenticatedRequest, res: Response) => Promise<void> { | ||||
|     return async (req, res) => { | ||||
|         const path: LearningPath = req.body; | ||||
|         if (isPut) { | ||||
|             if (req.params.hruid !== path.hruid || req.params.language !== path.language) { | ||||
|                 throw new BadRequestException("id_not_matching_query_params"); | ||||
|             } | ||||
|         } | ||||
|         const teacher = await getTeacher(req.auth!.username); | ||||
|         res.json(await learningPathService.createNewLearningPath(path, [teacher], isPut)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export const postLearningPath = postOrPutLearningPath(false); | ||||
| export const putLearningPath = postOrPutLearningPath(true); | ||||
| 
 | ||||
| export async function deleteLearningPath(req: AuthenticatedRequest, res: Response): Promise<void> { | ||||
|     const id: LearningPathIdentifier = { | ||||
|         hruid: req.params.hruid, | ||||
|         language: req.params.language as Language | ||||
|     }; | ||||
|     const deletedPath = await learningPathService.deleteLearningPath(id); | ||||
|     if (deletedPath) { | ||||
|         res.json(deletedPath); | ||||
|     } else { | ||||
|         throw new NotFoundException("The learning path could not be found."); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -28,6 +28,21 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath> | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns all learning paths which have the user with the given username as an administrator. | ||||
|      */ | ||||
|     public async findAllByAdminUsername(adminUsername: string): Promise<LearningPath[]> { | ||||
|         return this.findAll({ | ||||
|             where: { | ||||
|                 admins: { | ||||
|                     $contains: { | ||||
|                         username: adminUsername | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public createNode(nodeData: RequiredEntityData<LearningPathNode>): LearningPathNode { | ||||
|         return this.em.create(LearningPathNode, nodeData); | ||||
|     } | ||||
|  | @ -50,4 +65,16 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath> | |||
|         await Promise.all(nodes.map(async (it) => em.persistAndFlush(it))); | ||||
|         await Promise.all(transitions.map(async (it) => em.persistAndFlush(it))); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes the learning path with the given hruid and language. | ||||
|      * @returns the deleted learning path or null if it was not found. | ||||
|      */ | ||||
|     public async deleteByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { | ||||
|         const path = await this.findByHruidAndLanguage(hruid, language); | ||||
|         if (path) { | ||||
|             await this.em.removeAndFlush(path); | ||||
|         } | ||||
|         return path; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,12 @@ | |||
| import learningPathService from "../../../services/learning-paths/learning-path-service"; | ||||
| import { authorize } from "../auth"; | ||||
| import { AuthenticatedRequest } from "../authenticated-request"; | ||||
| import { AuthenticationInfo } from "../authentication-info"; | ||||
| 
 | ||||
| export const onlyAdminsForLearningPath = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||
|     const adminsForLearningPath = learningPathService.getAdmins({ | ||||
|         hruid: req.body.hruid, | ||||
|         language: req.body.language | ||||
|     }); | ||||
|     return adminsForLearningPath && auth.username in adminsForLearningPath; | ||||
| }); | ||||
|  | @ -1,5 +1,7 @@ | |||
| import express from 'express'; | ||||
| import { getLearningPaths } from '../controllers/learning-paths.js'; | ||||
| import { deleteLearningPath, getLearningPaths, postLearningPath, putLearningPath } from '../controllers/learning-paths.js'; | ||||
| import { teachersOnly } from '../middleware/auth/auth.js'; | ||||
| import { onlyAdminsForLearningObject } from '../middleware/auth/checks/learning-object-auth-checks.js'; | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
|  | @ -23,5 +25,9 @@ const router = express.Router(); | |||
| // Example: http://localhost:3000/learningPath?theme=kiks
 | ||||
| 
 | ||||
| router.get('/', getLearningPaths); | ||||
| router.post('/', teachersOnly, postLearningPath) | ||||
| 
 | ||||
| router.put('/:hruid/:language', onlyAdminsForLearningObject, putLearningPath); | ||||
| router.delete('/:hruid/:language', onlyAdminsForLearningObject, deleteLearningPath); | ||||
| 
 | ||||
| export default router; | ||||
|  |  | |||
|  | @ -198,6 +198,15 @@ const databaseLearningPathProvider: LearningPathProvider = { | |||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Returns all the learning paths which have the user with the given username as an administrator. | ||||
|      */ | ||||
|     async getLearningPathsAdministratedBy(adminUsername: string): Promise<LearningPath[]> { | ||||
|         const repo = getLearningPathRepository(); | ||||
|         const paths = await repo.findAllByAdminUsername(adminUsername); | ||||
|         return await Promise.all(paths.map(async (result, index) => convertLearningPath(result, index))); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Search learning paths in the database using the given search string. | ||||
|      */ | ||||
|  |  | |||
|  | @ -45,6 +45,10 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | |||
|         const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params }); | ||||
|         return searchResults ?? []; | ||||
|     }, | ||||
| 
 | ||||
|     async getLearningPathsAdministratedBy(_adminUsername: string) { | ||||
|         return []; // Learning paths fetched from the Dwengo API cannot be administrated by a user.
 | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| export default dwengoApiLearningPathProvider; | ||||
|  |  | |||
|  | @ -15,4 +15,9 @@ export interface LearningPathProvider { | |||
|      * Search learning paths in the data source using the given search string. | ||||
|      */ | ||||
|     searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]>; | ||||
| 
 | ||||
|     /** | ||||
|      * Get all learning paths which have the teacher with the given user as an administrator. | ||||
|      */ | ||||
|     getLearningPathsAdministratedBy(adminUsername: string): Promise<LearningPath[]>; | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; | ||||
| import databaseLearningPathProvider from './database-learning-path-provider.js'; | ||||
| import { envVars, getEnvVar } from '../../util/envVars.js'; | ||||
| import { LearningObjectNode, LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| import { LearningObjectNode, LearningPath, LearningPathIdentifier, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { Group } from '../../entities/assignments/group.entity.js'; | ||||
| import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.entity.js'; | ||||
|  | @ -12,6 +12,7 @@ import { base64ToArrayBuffer } from '../../util/base64-buffer-conversion.js'; | |||
| import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | ||||
| import { mapToTeacher } from '../../interfaces/teacher.js'; | ||||
| import { Collection } from '@mikro-orm/core'; | ||||
| import { NotFoundException } from '../../exceptions/not-found-exception.js'; | ||||
| 
 | ||||
| const userContentPrefix = getEnvVar(envVars.UserContentPrefix); | ||||
| const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider]; | ||||
|  | @ -105,6 +106,16 @@ const learningPathService = { | |||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the learning paths administrated by the teacher with the given username. | ||||
|      */ | ||||
|     async getLearningPathsAdministratedBy(adminUsername: string): Promise<LearningPath[]> { | ||||
|         const providerResponses = await Promise.all( | ||||
|             allProviders.map(async (provider) => provider.getLearningPathsAdministratedBy(adminUsername)) | ||||
|         ); | ||||
|         return providerResponses.flat(); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Search learning paths in the data source using the given search string. | ||||
|      */ | ||||
|  | @ -119,12 +130,42 @@ const learningPathService = { | |||
|      * Add a new learning path to the database. | ||||
|      * @param dto Learning path DTO from which the learning path will be created. | ||||
|      * @param admins Teachers who should become an admin of the learning path. | ||||
|      * @param allowReplace If this is set to true and there is already a learning path with the same identifier, it is replaced. | ||||
|      * @returns the created learning path. | ||||
|      */ | ||||
|     async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<void> { | ||||
|     async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[], allowReplace = false): Promise<LearningPathEntity> { | ||||
|         const repo = getLearningPathRepository(); | ||||
|         const path = mapToLearningPath(dto, admins); | ||||
|         await repo.save(path, { preventOverwrite: true }); | ||||
|         await repo.save(path, { preventOverwrite: allowReplace }); | ||||
|         return path; | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes the learning path with the given identifier from the database. | ||||
|      * @param id Identifier of the learning path to delete. | ||||
|      * @returns the deleted learning path. | ||||
|      */ | ||||
|     async deleteLearningPath(id: LearningPathIdentifier): Promise<LearningPathEntity> { | ||||
|         const repo = getLearningPathRepository(); | ||||
|         const deletedPath = await repo.deleteByHruidAndLanguage(id.hruid, id.language); | ||||
|         if (deletedPath) { | ||||
|             return deletedPath; | ||||
|         } | ||||
|         throw new NotFoundException("No learning path with the given identifier found."); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a list of the usernames of the administrators of the learning path with the given identifier. | ||||
|      * @param id The identifier of the learning path whose admins should be fetched. | ||||
|      */ | ||||
|     async getAdmins(id: LearningPathIdentifier): Promise<string[]> { | ||||
|         const repo = getLearningPathRepository(); | ||||
|         const path = await repo.findByHruidAndLanguage(id.hruid, id.language); | ||||
|         if (!path) { | ||||
|             throw new NotFoundException("No learning path with the given identifier found."); | ||||
|         } | ||||
|         return path.admins.map(admin => admin.username); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export default learningPathService; | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger