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,11 +7,19 @@ import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
| import { NotFoundException } from '../exceptions/not-found-exception.js'; | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
| import { Group } from '../entities/assignments/group.entity.js'; | import { Group } from '../entities/assignments/group.entity.js'; | ||||||
| import { getAssignmentRepository, getGroupRepository } from '../data/repositories.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. |  * Fetch learning paths based on query parameters. | ||||||
|  */ |  */ | ||||||
| export async function getLearningPaths(req: Request, res: Response): Promise<void> { | export async function getLearningPaths(req: Request, res: Response): Promise<void> { | ||||||
|  |     const admin = req.query.admin; | ||||||
|  |     if (admin) { | ||||||
|  |         const paths = await learningPathService.getLearningPathsAdministratedBy(admin as string); | ||||||
|  |         res.json(paths); | ||||||
|  |     } else { | ||||||
|         const hruids = req.query.hruid; |         const hruids = req.query.hruid; | ||||||
|         const themeKey = req.query.theme as string; |         const themeKey = req.query.theme as string; | ||||||
|         const searchQuery = req.query.search as string; |         const searchQuery = req.query.search as string; | ||||||
|  | @ -55,3 +63,33 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi | ||||||
|         const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup); |         const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup); | ||||||
|         res.json(learningPaths.data); |         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 { |     public createNode(nodeData: RequiredEntityData<LearningPathNode>): LearningPathNode { | ||||||
|         return this.em.create(LearningPathNode, nodeData); |         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(nodes.map(async (it) => em.persistAndFlush(it))); | ||||||
|         await Promise.all(transitions.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 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(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | @ -23,5 +25,9 @@ const router = express.Router(); | ||||||
| // Example: http://localhost:3000/learningPath?theme=kiks
 | // Example: http://localhost:3000/learningPath?theme=kiks
 | ||||||
| 
 | 
 | ||||||
| router.get('/', getLearningPaths); | router.get('/', getLearningPaths); | ||||||
|  | router.post('/', teachersOnly, postLearningPath) | ||||||
|  | 
 | ||||||
|  | router.put('/:hruid/:language', onlyAdminsForLearningObject, putLearningPath); | ||||||
|  | router.delete('/:hruid/:language', onlyAdminsForLearningObject, deleteLearningPath); | ||||||
| 
 | 
 | ||||||
| export default router; | 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. |      * 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 }); |         const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params }); | ||||||
|         return searchResults ?? []; |         return searchResults ?? []; | ||||||
|     }, |     }, | ||||||
|  | 
 | ||||||
|  |     async getLearningPathsAdministratedBy(_adminUsername: string) { | ||||||
|  |         return []; // Learning paths fetched from the Dwengo API cannot be administrated by a user.
 | ||||||
|  |     }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default dwengoApiLearningPathProvider; | export default dwengoApiLearningPathProvider; | ||||||
|  |  | ||||||
|  | @ -15,4 +15,9 @@ export interface LearningPathProvider { | ||||||
|      * Search learning paths in the data source using the given search string. |      * Search learning paths in the data source using the given search string. | ||||||
|      */ |      */ | ||||||
|     searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]>; |     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 dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; | ||||||
| import databaseLearningPathProvider from './database-learning-path-provider.js'; | import databaseLearningPathProvider from './database-learning-path-provider.js'; | ||||||
| import { envVars, getEnvVar } from '../../util/envVars.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 { Language } from '@dwengo-1/common/util/language'; | ||||||
| import { Group } from '../../entities/assignments/group.entity.js'; | import { Group } from '../../entities/assignments/group.entity.js'; | ||||||
| import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.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 { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | ||||||
| import { mapToTeacher } from '../../interfaces/teacher.js'; | import { mapToTeacher } from '../../interfaces/teacher.js'; | ||||||
| import { Collection } from '@mikro-orm/core'; | import { Collection } from '@mikro-orm/core'; | ||||||
|  | import { NotFoundException } from '../../exceptions/not-found-exception.js'; | ||||||
| 
 | 
 | ||||||
| const userContentPrefix = getEnvVar(envVars.UserContentPrefix); | const userContentPrefix = getEnvVar(envVars.UserContentPrefix); | ||||||
| const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider]; | 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. |      * 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. |      * Add a new learning path to the database. | ||||||
|      * @param dto Learning path DTO from which the learning path will be created. |      * @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 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 repo = getLearningPathRepository(); | ||||||
|         const path = mapToLearningPath(dto, admins); |         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; | export default learningPathService; | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger