feat: question route als subroute van learning objects
This commit is contained in:
		
							parent
							
								
									3e2c73320b
								
							
						
					
					
						commit
						3dd1edc95d
					
				
					 6 changed files with 310 additions and 49 deletions
				
			
		
							
								
								
									
										118
									
								
								backend/src/controllers/questions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								backend/src/controllers/questions.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,118 @@ | ||||||
|  | import {Request, Response} from "express"; | ||||||
|  | import { | ||||||
|  |     createQuestion, | ||||||
|  |     deleteQuestion, | ||||||
|  |     getAllQuestions, | ||||||
|  |     getAnswersByQuestion, | ||||||
|  |     getQuestion | ||||||
|  | } from "../services/questions.js"; | ||||||
|  | import {QuestionDTO, QuestionId} from "../interfaces/question.js"; | ||||||
|  | import {FALLBACK_LANG, FALLBACK_SEQ_NUM} from "../config.js"; | ||||||
|  | import {LearningObjectIdentifier} from "../entities/content/learning-object-identifier.js"; | ||||||
|  | import {Language} from "../entities/content/language.js"; | ||||||
|  | 
 | ||||||
|  | function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { | ||||||
|  |     const { hruid, version} = req.params | ||||||
|  |     const lang = req.query.lang | ||||||
|  | 
 | ||||||
|  |     if (!hruid || !version ) { | ||||||
|  |         res.status(400).json({ error: "Missing required parameters." }); | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         hruid, | ||||||
|  |         language: lang as Language || FALLBACK_LANG, | ||||||
|  |         version | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getQuestionId(req: Request, res: Response): QuestionId | null { | ||||||
|  |     const seq = req.params.seq | ||||||
|  |     const learningObjectIdentifier = getObjectId(req,res); | ||||||
|  | 
 | ||||||
|  |     if (!learningObjectIdentifier) | ||||||
|  |         return null | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         learningObjectIdentifier, | ||||||
|  |         sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getAllQuestionsHandler( | ||||||
|  |     req: Request, | ||||||
|  |     res: Response | ||||||
|  | ): Promise<void> { | ||||||
|  |     const objectId = getObjectId(req, res); | ||||||
|  |     const full = req.query.full === 'true'; | ||||||
|  | 
 | ||||||
|  |     if (!objectId) | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     const questions = await getAllQuestions(objectId, full); | ||||||
|  | 
 | ||||||
|  |     if (!questions) | ||||||
|  |         res.status(404).json({ error: `Questions not found.` }); | ||||||
|  |     else | ||||||
|  |         res.json(questions); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const questionId = getQuestionId(req, res); | ||||||
|  | 
 | ||||||
|  |     if (!questionId) | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     const question = await getQuestion(questionId); | ||||||
|  | 
 | ||||||
|  |     if (!question) | ||||||
|  |         res.status(404).json({ error: `Question not found.` }); | ||||||
|  |     else | ||||||
|  |         res.json(question) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getQuestionAnswersHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const questionId = getQuestionId(req, res); | ||||||
|  |     const full = req.query.full === 'true'; | ||||||
|  | 
 | ||||||
|  |     if (!questionId) | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     const answers = getAnswersByQuestion(questionId, full); | ||||||
|  | 
 | ||||||
|  |     if (!answers) | ||||||
|  |         res.status(404).json({ error: `Questions not found.` }); | ||||||
|  |     else | ||||||
|  |         res.json(answers) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const questionDTO = req.body as QuestionDTO; | ||||||
|  | 
 | ||||||
|  |     if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) { | ||||||
|  |         res.status(400).json({error: 'Missing required fields: identifier and content'}); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const question = await createQuestion(questionDTO); | ||||||
|  | 
 | ||||||
|  |     if (!question) | ||||||
|  |         res.status(400).json({error: 'Could not add question'}); | ||||||
|  |     else | ||||||
|  |         res.json(question) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const questionId = getQuestionId(res, req) | ||||||
|  | 
 | ||||||
|  |     const question = await deleteQuestion(questionId); | ||||||
|  | 
 | ||||||
|  |     if (!question) | ||||||
|  |         res.status(400).json({error: 'Could not find nor delete question'}); | ||||||
|  |     else | ||||||
|  |         res.json(question) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
							
								
								
									
										38
									
								
								backend/src/interfaces/answer.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								backend/src/interfaces/answer.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | ||||||
|  | import {mapToUserDTO, UserDTO} from "./user.js"; | ||||||
|  | import {mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId} from "./question.js"; | ||||||
|  | import {Answer} from "../entities/questions/answer.entity.js"; | ||||||
|  | 
 | ||||||
|  | export interface AnswerDTO { | ||||||
|  |     author: UserDTO; | ||||||
|  |     toQuestion: QuestionDTO; | ||||||
|  |     sequenceNumber: number; | ||||||
|  |     timestamp: string; | ||||||
|  |     content: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Convert a Question entity to a DTO format. | ||||||
|  |  */ | ||||||
|  | export function mapToAnswerDTO(answer: Answer): AnswerDTO { | ||||||
|  |     return { | ||||||
|  |         author: mapToUserDTO(answer.author), | ||||||
|  |         toQuestion: mapToQuestionDTO(answer.toQuestion), | ||||||
|  |         sequenceNumber: answer.sequenceNumber, | ||||||
|  |         timestamp: answer.timestamp.toISOString(), | ||||||
|  |         content: answer.content, | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface AnswerId { | ||||||
|  |     author: string; | ||||||
|  |     toQuestion: QuestionId; | ||||||
|  |     sequenceNumber: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function mapToAnswerId(answer: AnswerDTO): AnswerId { | ||||||
|  |     return { | ||||||
|  |         author: answer.author.username, | ||||||
|  |         toQuestion: mapToQuestionId(answer.toQuestion), | ||||||
|  |         sequenceNumber: answer.sequenceNumber | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,48 +1,56 @@ | ||||||
| import { Question } from '../entities/questions/question.entity.js'; | import { Question } from '../entities/questions/question.entity.js'; | ||||||
|  | import {mapToUserDTO, UserDTO} from "./user.js"; | ||||||
|  | import {LearningObjectIdentifier} from "../entities/content/learning-object-identifier.js"; | ||||||
|  | import {Teacher} from "../entities/users/teacher.entity"; | ||||||
| 
 | 
 | ||||||
| export interface QuestionDTO { | export interface QuestionDTO { | ||||||
|     learningObjectHruid: string; |     learningObjectIdentifier: LearningObjectIdentifier; | ||||||
|     learningObjectLanguage: string; |     sequenceNumber?: number; | ||||||
|     learningObjectVersion: string; |     author: UserDTO; | ||||||
|     sequenceNumber: number; |     timestamp?: string; | ||||||
|     authorUsername: string; |  | ||||||
|     timestamp: string; |  | ||||||
|     content: string; |     content: string; | ||||||
|     endpoints?: { |  | ||||||
|         classes: string; |  | ||||||
|         questions: string; |  | ||||||
|         invitations: string; |  | ||||||
|         groups: string; |  | ||||||
|     }; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Convert a Question entity to a DTO format. |  * Convert a Question entity to a DTO format. | ||||||
|  */ |  */ | ||||||
| export function mapToQuestionDTO(question: Question): QuestionDTO { | export function mapToQuestionDTO(question: Question): QuestionDTO { | ||||||
|  |     const learningObjectIdentifier = { | ||||||
|  |         hruid: question.learningObjectHruid, | ||||||
|  |         language: question.learningObjectLanguage, | ||||||
|  |         version: question.learningObjectVersion | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     return { |     return { | ||||||
|         learningObjectHruid: question.learningObjectHruid, |         learningObjectIdentifier, | ||||||
|         learningObjectLanguage: question.learningObjectLanguage, |  | ||||||
|         learningObjectVersion: question.learningObjectVersion, |  | ||||||
|         sequenceNumber: question.sequenceNumber, |         sequenceNumber: question.sequenceNumber, | ||||||
|         authorUsername: question.author.username, |         author: question.author, | ||||||
|         timestamp: question.timestamp.toISOString(), |         timestamp: question.timestamp.toISOString(), | ||||||
|         content: question.content, |         content: question.content, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface QuestionId { | export interface QuestionId { | ||||||
|     learningObjectHruid: string; |     learningObjectIdentifier: LearningObjectIdentifier; | ||||||
|     learningObjectLanguage: string; |  | ||||||
|     learningObjectVersion: string; |  | ||||||
|     sequenceNumber: number; |     sequenceNumber: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToQuestionId(question: QuestionDTO): QuestionId { | export function mapToQuestionId(question: QuestionDTO): QuestionId { | ||||||
|     return { |     return { | ||||||
|         learningObjectHruid: question.learningObjectHruid, |         learningObjectIdentifier: question.learningObjectIdentifier, | ||||||
|         learningObjectLanguage: question.learningObjectLanguage, |  | ||||||
|         learningObjectVersion: question.learningObjectVersion, |  | ||||||
|         sequenceNumber: question.sequenceNumber, |         sequenceNumber: question.sequenceNumber, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function mapToQuestion(questionDTO: QuestionDTO): Question { | ||||||
|  |     const question = new Question(); | ||||||
|  |     question.author = mapToUserDTO<Teacher>(questionDTO.author); | ||||||
|  |     question.learningObjectHruid = questionDTO.learningObjectIdentifier.hruid; | ||||||
|  |     question.learningObjectLanguage = questionDTO.learningObjectIdentifier.language; | ||||||
|  |     question.learningObjectVersion = questionDTO.learningObjectIdentifier.version; | ||||||
|  |     question.content  = questionDTO.content; | ||||||
|  |     question.sequenceNumber = questionDTO.sequenceNumber; | ||||||
|  |     question.timestamp = questionDTO.timestamp; | ||||||
|  | 
 | ||||||
|  |     return question; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ import { | ||||||
| } from '../controllers/learning-objects.js'; | } from '../controllers/learning-objects.js'; | ||||||
| 
 | 
 | ||||||
| import submissionRoutes from './submissions.js'; | import submissionRoutes from './submissions.js'; | ||||||
|  | import questionRoutes from './questions.js'; | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | @ -28,4 +30,6 @@ router.get('/:hruid', getLearningObject); | ||||||
| 
 | 
 | ||||||
| router.use('/:hruid/submissions', submissionRoutes); | router.use('/:hruid/submissions', submissionRoutes); | ||||||
| 
 | 
 | ||||||
|  | router.use('/:hruid/:version/questions', questionRoutes) | ||||||
|  | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -1,34 +1,24 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| const router = express.Router(); | import { | ||||||
|  |     createQuestionHandler, deleteQuestionHandler, | ||||||
|  |     getAllQuestionsHandler, | ||||||
|  |     getQuestionAnswersHandler, | ||||||
|  |     getQuestionHandler | ||||||
|  | } from "../controllers/questions.js"; | ||||||
|  | const router = express.Router({ mergeParams: true }); | ||||||
|  | 
 | ||||||
|  | // query language
 | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | // Root endpoint used to search objects
 | ||||||
| router.get('/', (req, res) => { | router.get('/', getAllQuestionsHandler); | ||||||
|     res.json({ |  | ||||||
|         questions: ['0', '1'], |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| // Information about an question with id 'id'
 | router.post('/', createQuestionHandler); | ||||||
| router.get('/:id', (req, res) => { |  | ||||||
|     res.json({ |  | ||||||
|         id: req.params.id, |  | ||||||
|         student: '0', |  | ||||||
|         group: '0', |  | ||||||
|         time: new Date(2025, 1, 1), |  | ||||||
|         content: |  | ||||||
|             'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????', |  | ||||||
|         learningObject: '0', |  | ||||||
|         links: { |  | ||||||
|             self: `${req.baseUrl}/${req.params.id}`, |  | ||||||
|             answers: `${req.baseUrl}/${req.params.id}/answers`, |  | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| router.get('/:id/answers', (req, res) => { | router.delete('/:seq', deleteQuestionHandler); | ||||||
|     res.json({ | 
 | ||||||
|         answers: ['0'], | // Information about a question with id
 | ||||||
|     }); | router.get('/:seq', getQuestionHandler); | ||||||
| }); | 
 | ||||||
|  | router.get('/answers/:seq', getQuestionAnswersHandler); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
							
								
								
									
										103
									
								
								backend/src/services/questions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								backend/src/services/questions.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,103 @@ | ||||||
|  | import {getAnswerRepository, getQuestionRepository} from "../data/repositories.js"; | ||||||
|  | import {mapToQuestion, mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId} from "../interfaces/question.js"; | ||||||
|  | import {Question} from "../entities/questions/question.entity.js"; | ||||||
|  | import {Answer} from "../entities/questions/answer.entity.js"; | ||||||
|  | import {mapToAnswerDTO, mapToAnswerId} from "../interfaces/answer.js"; | ||||||
|  | import {QuestionRepository} from "../data/questions/question-repository.js"; | ||||||
|  | import {LearningObjectIdentifier} from "../entities/content/learning-object-identifier.js"; | ||||||
|  | 
 | ||||||
|  | export async function getAllQuestions( | ||||||
|  |     id: LearningObjectIdentifier, full: boolean | ||||||
|  | ): Promise<QuestionDTO[] | QuestionId[]> { | ||||||
|  |     const questionRepository: QuestionRepository = getQuestionRepository(); | ||||||
|  |     const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); | ||||||
|  | 
 | ||||||
|  |     if (!questions) { | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const questionsDTO: QuestionDTO[]  = questions.map(mapToQuestionDTO); | ||||||
|  | 
 | ||||||
|  |     if (full) { | ||||||
|  |         return questionsDTO; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return questionsDTO.map(mapToQuestionId); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchQuestion(questionId: QuestionId): Promise<Question | null> { | ||||||
|  |     const questionRepository = getQuestionRepository(); | ||||||
|  | 
 | ||||||
|  |     return await questionRepository.findOne( | ||||||
|  |         { | ||||||
|  |             learningObjectHruid: questionId.learningObjectIdentifier.hruid, | ||||||
|  |             learningObjectLanguage: questionId.learningObjectIdentifier.language, | ||||||
|  |             learningObjectVersion: questionId.learningObjectIdentifier.version, | ||||||
|  |             sequenceNumber: questionId.sequenceNumber | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO | null> { | ||||||
|  |     const question = await fetchQuestion(questionId); | ||||||
|  | 
 | ||||||
|  |     if (!question) | ||||||
|  |         return null | ||||||
|  | 
 | ||||||
|  |     return mapToQuestionDTO(question); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getAnswersByQuestion(questionId: QuestionId, full: boolean) { | ||||||
|  |     const answerRepository = getAnswerRepository(); | ||||||
|  |     const question = await fetchQuestion(questionId); | ||||||
|  | 
 | ||||||
|  |     if (!question) | ||||||
|  |         return []; | ||||||
|  | 
 | ||||||
|  |     const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question); | ||||||
|  | 
 | ||||||
|  |     if (!answers) | ||||||
|  |         return [] | ||||||
|  | 
 | ||||||
|  |     const answersDTO = answers.map(mapToAnswerDTO); | ||||||
|  | 
 | ||||||
|  |     if (full) | ||||||
|  |         return answersDTO | ||||||
|  | 
 | ||||||
|  |     return answersDTO.map(mapToAnswerId); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createQuestion(questionDTO: QuestionDTO) { | ||||||
|  |     const questionRepository = getQuestionRepository(); | ||||||
|  |     const question = mapToQuestion(questionDTO); | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |         const newQuestion = questionRepository.create(question) | ||||||
|  |         await questionRepository.save(newQuestion); | ||||||
|  |     } catch (e) { | ||||||
|  |         return null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return newQuestion; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteQuestion(questionId: QuestionId) { | ||||||
|  |     const questionRepository = getQuestionRepository(); | ||||||
|  | 
 | ||||||
|  |     const question = await fetchQuestion(questionId); | ||||||
|  | 
 | ||||||
|  |     if (!question) | ||||||
|  |         return null | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |         await questionRepository.removeQuestionByLearningObjectAndSequenceNumber( | ||||||
|  |             questionId.learningObjectIdentifier, questionId.sequenceNumber | ||||||
|  |         ); | ||||||
|  |     } catch (e) { | ||||||
|  |         return null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return question | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl