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 {mapToUserDTO, UserDTO} from "./user.js"; | ||||
| import {LearningObjectIdentifier} from "../entities/content/learning-object-identifier.js"; | ||||
| import {Teacher} from "../entities/users/teacher.entity"; | ||||
| 
 | ||||
| export interface QuestionDTO { | ||||
|     learningObjectHruid: string; | ||||
|     learningObjectLanguage: string; | ||||
|     learningObjectVersion: string; | ||||
|     sequenceNumber: number; | ||||
|     authorUsername: string; | ||||
|     timestamp: string; | ||||
|     learningObjectIdentifier: LearningObjectIdentifier; | ||||
|     sequenceNumber?: number; | ||||
|     author: UserDTO; | ||||
|     timestamp?: string; | ||||
|     content: string; | ||||
|     endpoints?: { | ||||
|         classes: string; | ||||
|         questions: string; | ||||
|         invitations: string; | ||||
|         groups: string; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Convert a Question entity to a DTO format. | ||||
|  */ | ||||
| export function mapToQuestionDTO(question: Question): QuestionDTO { | ||||
|     const learningObjectIdentifier = { | ||||
|         hruid: question.learningObjectHruid, | ||||
|         language: question.learningObjectLanguage, | ||||
|         version: question.learningObjectVersion | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         learningObjectHruid: question.learningObjectHruid, | ||||
|         learningObjectLanguage: question.learningObjectLanguage, | ||||
|         learningObjectVersion: question.learningObjectVersion, | ||||
|         learningObjectIdentifier, | ||||
|         sequenceNumber: question.sequenceNumber, | ||||
|         authorUsername: question.author.username, | ||||
|         author: question.author, | ||||
|         timestamp: question.timestamp.toISOString(), | ||||
|         content: question.content, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export interface QuestionId { | ||||
|     learningObjectHruid: string; | ||||
|     learningObjectLanguage: string; | ||||
|     learningObjectVersion: string; | ||||
|     learningObjectIdentifier: LearningObjectIdentifier; | ||||
|     sequenceNumber: number; | ||||
| } | ||||
| 
 | ||||
| export function mapToQuestionId(question: QuestionDTO): QuestionId { | ||||
|     return { | ||||
|         learningObjectHruid: question.learningObjectHruid, | ||||
|         learningObjectLanguage: question.learningObjectLanguage, | ||||
|         learningObjectVersion: question.learningObjectVersion, | ||||
|         learningObjectIdentifier: question.learningObjectIdentifier, | ||||
|         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'; | ||||
| 
 | ||||
| import submissionRoutes from './submissions.js'; | ||||
| import questionRoutes from './questions.js'; | ||||
| 
 | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
|  | @ -28,4 +30,6 @@ router.get('/:hruid', getLearningObject); | |||
| 
 | ||||
| router.use('/:hruid/submissions', submissionRoutes); | ||||
| 
 | ||||
| router.use('/:hruid/:version/questions', questionRoutes) | ||||
| 
 | ||||
| export default router; | ||||
|  |  | |||
|  | @ -1,34 +1,24 @@ | |||
| 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
 | ||||
| router.get('/', (req, res) => { | ||||
|     res.json({ | ||||
|         questions: ['0', '1'], | ||||
|     }); | ||||
| }); | ||||
| router.get('/', getAllQuestionsHandler); | ||||
| 
 | ||||
| // Information about an question with id 'id'
 | ||||
| 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.post('/', createQuestionHandler); | ||||
| 
 | ||||
| router.get('/:id/answers', (req, res) => { | ||||
|     res.json({ | ||||
|         answers: ['0'], | ||||
|     }); | ||||
| }); | ||||
| router.delete('/:seq', deleteQuestionHandler); | ||||
| 
 | ||||
| // Information about a question with id
 | ||||
| router.get('/:seq', getQuestionHandler); | ||||
| 
 | ||||
| router.get('/answers/:seq', getQuestionAnswersHandler); | ||||
| 
 | ||||
| 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