From 3dd1edc95de924b6a1e7b3355f4e9d07af4f020c Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Thu, 13 Mar 2025 15:02:11 +0100 Subject: [PATCH] feat: question route als subroute van learning objects --- backend/src/controllers/questions.ts | 118 +++++++++++++++++++++++++ backend/src/interfaces/answer.ts | 38 ++++++++ backend/src/interfaces/question.ts | 52 ++++++----- backend/src/routes/learning-objects.ts | 4 + backend/src/routes/questions.ts | 44 ++++----- backend/src/services/questions.ts | 103 +++++++++++++++++++++ 6 files changed, 310 insertions(+), 49 deletions(-) create mode 100644 backend/src/controllers/questions.ts create mode 100644 backend/src/interfaces/answer.ts create mode 100644 backend/src/services/questions.ts diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts new file mode 100644 index 00000000..850dcdf7 --- /dev/null +++ b/backend/src/controllers/questions.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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) +} + + + diff --git a/backend/src/interfaces/answer.ts b/backend/src/interfaces/answer.ts new file mode 100644 index 00000000..1b363c0d --- /dev/null +++ b/backend/src/interfaces/answer.ts @@ -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 + } +} diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 90de4999..33dced69 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -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(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; +} diff --git a/backend/src/routes/learning-objects.ts b/backend/src/routes/learning-objects.ts index 77094955..6b708d4b 100644 --- a/backend/src/routes/learning-objects.ts +++ b/backend/src/routes/learning-objects.ts @@ -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; diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index f683d998..d635a85e 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -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; diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts new file mode 100644 index 00000000..e3c64bbb --- /dev/null +++ b/backend/src/services/questions.ts @@ -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 { + 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 { + 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 { + 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 +} + +