Merge pull request #171 from SELab-2/feat/question-routes
feat: Question en Answer routes
This commit is contained in:
		
						commit
						1405fd66d1
					
				
					 32 changed files with 815 additions and 218 deletions
				
			
		|  | @ -5,3 +5,4 @@ export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl); | ||||||
| export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage); | export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage); | ||||||
| 
 | 
 | ||||||
| export const FALLBACK_SEQ_NUM = 1; | export const FALLBACK_SEQ_NUM = 1; | ||||||
|  | export const FALLBACK_VERSION_NUM = 1; | ||||||
|  |  | ||||||
							
								
								
									
										99
									
								
								backend/src/controllers/answers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								backend/src/controllers/answers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | ||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import { requireFields } from './error-helper.js'; | ||||||
|  | import { getLearningObjectId, getQuestionId } from './questions.js'; | ||||||
|  | import { createAnswer, deleteAnswer, getAnswer, getAnswersByQuestion, updateAnswer } from '../services/answers.js'; | ||||||
|  | import { FALLBACK_SEQ_NUM } from '../config.js'; | ||||||
|  | import { AnswerData } from '@dwengo-1/common/interfaces/answer'; | ||||||
|  | 
 | ||||||
|  | export async function getAllAnswersHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     const full = req.query.full === 'true'; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const answers = await getAnswersByQuestion(questionId, full); | ||||||
|  | 
 | ||||||
|  |     res.json({ answers }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getAnswerHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     const seqAnswer = req.params.seqAnswer; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; | ||||||
|  |     const answer = await getAnswer(questionId, sequenceNumber); | ||||||
|  | 
 | ||||||
|  |     res.json({ answer }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createAnswerHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const author = req.body.author as string; | ||||||
|  |     const content = req.body.content as string; | ||||||
|  |     requireFields({ author, content }); | ||||||
|  | 
 | ||||||
|  |     const answerData = req.body as AnswerData; | ||||||
|  | 
 | ||||||
|  |     const answer = await createAnswer(questionId, answerData); | ||||||
|  | 
 | ||||||
|  |     res.json({ answer }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteAnswerHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     const seqAnswer = req.params.seqAnswer; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; | ||||||
|  |     const answer = await deleteAnswer(questionId, sequenceNumber); | ||||||
|  | 
 | ||||||
|  |     res.json({ answer }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function updateAnswerHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     const seqAnswer = req.params.seqAnswer; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const content = req.body.content as string; | ||||||
|  |     requireFields({ content }); | ||||||
|  | 
 | ||||||
|  |     const answerData = req.body as AnswerData; | ||||||
|  | 
 | ||||||
|  |     const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; | ||||||
|  |     const answer = await updateAnswer(questionId, sequenceNumber, answerData); | ||||||
|  | 
 | ||||||
|  |     res.json({ answer }); | ||||||
|  | } | ||||||
|  | @ -6,9 +6,9 @@ import attachmentService from '../services/learning-objects/attachment-service.j | ||||||
| import { BadRequestException } from '../exceptions/bad-request-exception.js'; | 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 { envVars, getEnvVar } from '../util/envVars.js'; | import { envVars, getEnvVar } from '../util/envVars.js'; | ||||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| 
 | 
 | ||||||
| function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO { | ||||||
|     if (!req.params.hruid) { |     if (!req.params.hruid) { | ||||||
|         throw new BadRequestException('HRUID is required.'); |         throw new BadRequestException('HRUID is required.'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,34 +1,20 @@ | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; | import { createQuestion, deleteQuestion, getAllQuestions, getQuestion, updateQuestion } from '../services/questions.js'; | ||||||
| import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; | import { FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM } from '../config.js'; | ||||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
| import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | import { QuestionData, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||||
| import { Language } from '@dwengo-1/common/util/language'; | import { Language } from '@dwengo-1/common/util/language'; | ||||||
|  | import { requireFields } from './error-helper.js'; | ||||||
| 
 | 
 | ||||||
| function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { | export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier { | ||||||
|     const { hruid, version } = req.params; |  | ||||||
|     const lang = req.query.lang; |  | ||||||
| 
 |  | ||||||
|     if (!hruid || !version) { |  | ||||||
|         res.status(400).json({ error: 'Missing required parameters.' }); |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return { |     return { | ||||||
|         hruid, |         hruid, | ||||||
|         language: (lang as Language) || FALLBACK_LANG, |         language: (lang || FALLBACK_LANG) as Language, | ||||||
|         version: Number(version), |         version: Number(version) || FALLBACK_VERSION_NUM, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getQuestionId(req: Request, res: Response): QuestionId | null { | export function getQuestionId(learningObjectIdentifier: LearningObjectIdentifier, seq: string): QuestionId { | ||||||
|     const seq = req.params.seq; |  | ||||||
|     const learningObjectIdentifier = getObjectId(req, res); |  | ||||||
| 
 |  | ||||||
|     if (!learningObjectIdentifier) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return { |     return { | ||||||
|         learningObjectIdentifier, |         learningObjectIdentifier, | ||||||
|         sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, |         sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, | ||||||
|  | @ -36,84 +22,84 @@ function getQuestionId(req: Request, res: Response): QuestionId | null { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> { | export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const objectId = getObjectId(req, res); |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|  |     requireFields({ hruid }); | ||||||
| 
 | 
 | ||||||
|     if (!objectId) { |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const questions = await getAllQuestions(objectId, full); |     const questions = await getAllQuestions(learningObjectId, full); | ||||||
| 
 | 
 | ||||||
|     if (!questions) { |     res.json({ questions }); | ||||||
|         res.status(404).json({ error: `Questions not found.` }); |  | ||||||
|     } else { |  | ||||||
|         res.json({ questions: questions }); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getQuestionHandler(req: Request, res: Response): Promise<void> { | export async function getQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const questionId = getQuestionId(req, res); |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     requireFields({ hruid }); | ||||||
| 
 | 
 | ||||||
|     if (!questionId) { |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|         return; |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const question = await getQuestion(questionId); |     const question = await getQuestion(questionId); | ||||||
| 
 | 
 | ||||||
|     if (!question) { |     res.json({ 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 = await getAnswersByQuestion(questionId, full); |  | ||||||
| 
 |  | ||||||
|     if (!answers) { |  | ||||||
|         res.status(404).json({ error: `Questions not found` }); |  | ||||||
|     } else { |  | ||||||
|         res.json({ answers: answers }); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createQuestionHandler(req: Request, res: Response): Promise<void> { | export async function createQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const questionDTO = req.body as QuestionDTO; |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     requireFields({ hruid }); | ||||||
| 
 | 
 | ||||||
|     if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) { |     const loId = getLearningObjectId(hruid, version, language); | ||||||
|         res.status(400).json({ error: 'Missing required fields: identifier and content' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const question = await createQuestion(questionDTO); |     const author = req.body.author as string; | ||||||
|  |     const content = req.body.content as string; | ||||||
|  |     requireFields({ author, content }); | ||||||
| 
 | 
 | ||||||
|     if (!question) { |     const questionData = req.body as QuestionData; | ||||||
|         res.status(400).json({ error: 'Could not create question' }); | 
 | ||||||
|     } else { |     const question = await createQuestion(loId, questionData); | ||||||
|         res.json(question); | 
 | ||||||
|     } |     res.json({ question }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> { | export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const questionId = getQuestionId(req, res); |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     requireFields({ hruid }); | ||||||
| 
 | 
 | ||||||
|     if (!questionId) { |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|         return; |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const question = await deleteQuestion(questionId); |     const question = await deleteQuestion(questionId); | ||||||
| 
 | 
 | ||||||
|     if (!question) { |     res.json({ question }); | ||||||
|         res.status(400).json({ error: 'Could not find nor delete question' }); | } | ||||||
|     } else { | 
 | ||||||
|         res.json(question); | export async function updateQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     } |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const content = req.body.content as string; | ||||||
|  |     requireFields({ content }); | ||||||
|  | 
 | ||||||
|  |     const questionData = req.body as QuestionData; | ||||||
|  | 
 | ||||||
|  |     const question = await updateQuestion(questionId, questionData); | ||||||
|  | 
 | ||||||
|  |     res.json({ question }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| import { Answer } from '../../entities/questions/answer.entity.js'; | import { Answer } from '../../entities/questions/answer.entity.js'; | ||||||
| import { Question } from '../../entities/questions/question.entity.js'; | import { Question } from '../../entities/questions/question.entity.js'; | ||||||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
|  | import { Loaded } from '@mikro-orm/core'; | ||||||
| 
 | 
 | ||||||
| export class AnswerRepository extends DwengoEntityRepository<Answer> { | export class AnswerRepository extends DwengoEntityRepository<Answer> { | ||||||
|     public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> { |     public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> { | ||||||
|  | @ -19,10 +20,21 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> { | ||||||
|             orderBy: { sequenceNumber: 'ASC' }, |             orderBy: { sequenceNumber: 'ASC' }, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |     public async findAnswer(question: Question, sequenceNumber: number): Promise<Loaded<Answer> | null> { | ||||||
|  |         return this.findOne({ | ||||||
|  |             toQuestion: question, | ||||||
|  |             sequenceNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|     public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { |     public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { | ||||||
|         return this.deleteWhere({ |         return this.deleteWhere({ | ||||||
|             toQuestion: question, |             toQuestion: question, | ||||||
|             sequenceNumber: sequenceNumber, |             sequenceNumber: sequenceNumber, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |     public async updateContent(answer: Answer, newContent: string): Promise<Answer> { | ||||||
|  |         answer.content = newContent; | ||||||
|  |         await this.save(answer); | ||||||
|  |         return answer; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { Question } from '../../entities/questions/question.entity.js'; | ||||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | import { Student } from '../../entities/users/student.entity.js'; | ||||||
| import { LearningObject } from '../../entities/content/learning-object.entity.js'; | import { LearningObject } from '../../entities/content/learning-object.entity.js'; | ||||||
|  | import { Loaded } from '@mikro-orm/core'; | ||||||
| 
 | 
 | ||||||
| export class QuestionRepository extends DwengoEntityRepository<Question> { | export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|     public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> { |     public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> { | ||||||
|  | @ -61,4 +62,19 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|             orderBy: { timestamp: 'DESC' }, // New to old
 |             orderBy: { timestamp: 'DESC' }, // New to old
 | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<Loaded<Question> | null> { | ||||||
|  |         return this.findOne({ | ||||||
|  |             learningObjectHruid: loId.hruid, | ||||||
|  |             learningObjectLanguage: loId.language, | ||||||
|  |             learningObjectVersion: loId.version, | ||||||
|  |             sequenceNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async updateContent(question: Question, newContent: string): Promise<Question> { | ||||||
|  |         question.content = newContent; | ||||||
|  |         await this.save(question); | ||||||
|  |         return question; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,14 +1,14 @@ | ||||||
| import { mapToUserDTO } from './user.js'; |  | ||||||
| import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; | import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; | ||||||
| import { Answer } from '../entities/questions/answer.entity.js'; | import { Answer } from '../entities/questions/answer.entity.js'; | ||||||
| import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; | import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; | ||||||
|  | import { mapToTeacherDTO } from './teacher.js'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Convert a Question entity to a DTO format. |  * Convert a Question entity to a DTO format. | ||||||
|  */ |  */ | ||||||
| export function mapToAnswerDTO(answer: Answer): AnswerDTO { | export function mapToAnswerDTO(answer: Answer): AnswerDTO { | ||||||
|     return { |     return { | ||||||
|         author: mapToUserDTO(answer.author), |         author: mapToTeacherDTO(answer.author), | ||||||
|         toQuestion: mapToQuestionDTO(answer.toQuestion), |         toQuestion: mapToQuestionDTO(answer.toQuestion), | ||||||
|         sequenceNumber: answer.sequenceNumber!, |         sequenceNumber: answer.sequenceNumber!, | ||||||
|         timestamp: answer.timestamp.toISOString(), |         timestamp: answer.timestamp.toISOString(), | ||||||
|  |  | ||||||
|  | @ -1,9 +1,10 @@ | ||||||
| import { Question } from '../entities/questions/question.entity.js'; | import { Question } from '../entities/questions/question.entity.js'; | ||||||
| import { mapToStudentDTO } from './student.js'; | import { mapToStudentDTO } from './student.js'; | ||||||
| import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||||
| import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
|  | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
| 
 | 
 | ||||||
| function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { | function getLearningObjectIdentifier(question: Question): LearningObjectIdentifierDTO { | ||||||
|     return { |     return { | ||||||
|         hruid: question.learningObjectHruid, |         hruid: question.learningObjectHruid, | ||||||
|         language: question.learningObjectLanguage, |         language: question.learningObjectLanguage, | ||||||
|  | @ -11,6 +12,14 @@ function getLearningObjectIdentifier(question: Question): LearningObjectIdentifi | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function mapToLearningObjectID(loID: LearningObjectIdentifierDTO): LearningObjectIdentifier { | ||||||
|  |     return { | ||||||
|  |         hruid: loID.hruid, | ||||||
|  |         language: loID.language, | ||||||
|  |         version: loID.version ?? 1, | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Convert a Question entity to a DTO format. |  * Convert a Question entity to a DTO format. | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								backend/src/routes/answers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								backend/src/routes/answers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | import express from 'express'; | ||||||
|  | import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; | ||||||
|  | 
 | ||||||
|  | const router = express.Router({ mergeParams: true }); | ||||||
|  | 
 | ||||||
|  | router.get('/', getAllAnswersHandler); | ||||||
|  | 
 | ||||||
|  | router.post('/', createAnswerHandler); | ||||||
|  | 
 | ||||||
|  | router.get('/:seqAnswer', getAnswerHandler); | ||||||
|  | 
 | ||||||
|  | router.delete('/:seqAnswer', deleteAnswerHandler); | ||||||
|  | 
 | ||||||
|  | router.put('/:seqAnswer', updateAnswerHandler); | ||||||
|  | 
 | ||||||
|  | export default router; | ||||||
|  | @ -1,11 +1,7 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { | import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; | ||||||
|     createQuestionHandler, | import answerRoutes from './answers.js'; | ||||||
|     deleteQuestionHandler, | 
 | ||||||
|     getAllQuestionsHandler, |  | ||||||
|     getQuestionAnswersHandler, |  | ||||||
|     getQuestionHandler, |  | ||||||
| } from '../controllers/questions.js'; |  | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| // Query language
 | // Query language
 | ||||||
|  | @ -20,6 +16,6 @@ router.delete('/:seq', deleteQuestionHandler); | ||||||
| // Information about a question with id
 | // Information about a question with id
 | ||||||
| router.get('/:seq', getQuestionHandler); | router.get('/:seq', getQuestionHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/answers/:seq', getQuestionAnswersHandler); | router.use('/:seq/answers', answerRoutes); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
							
								
								
									
										70
									
								
								backend/src/services/answers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								backend/src/services/answers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | ||||||
|  | import { getAnswerRepository } from '../data/repositories.js'; | ||||||
|  | import { Answer } from '../entities/questions/answer.entity.js'; | ||||||
|  | import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js'; | ||||||
|  | import { fetchTeacher } from './teachers.js'; | ||||||
|  | import { fetchQuestion } from './questions.js'; | ||||||
|  | import { QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||||
|  | import { AnswerData, AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; | ||||||
|  | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
|  | 
 | ||||||
|  | export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise<AnswerDTO[] | AnswerId[]> { | ||||||
|  |     const answerRepository = getAnswerRepository(); | ||||||
|  |     const question = await fetchQuestion(questionId); | ||||||
|  | 
 | ||||||
|  |     const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question); | ||||||
|  | 
 | ||||||
|  |     if (full) { | ||||||
|  |         return answers.map(mapToAnswerDTO); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return answers.map(mapToAnswerDTOId); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createAnswer(questionId: QuestionId, answerData: AnswerData): Promise<AnswerDTO> { | ||||||
|  |     const answerRepository = getAnswerRepository(); | ||||||
|  |     const toQuestion = await fetchQuestion(questionId); | ||||||
|  |     const author = await fetchTeacher(answerData.author); | ||||||
|  |     const content = answerData.content; | ||||||
|  | 
 | ||||||
|  |     const answer = await answerRepository.createAnswer({ | ||||||
|  |         toQuestion, | ||||||
|  |         author, | ||||||
|  |         content, | ||||||
|  |     }); | ||||||
|  |     return mapToAnswerDTO(answer); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> { | ||||||
|  |     const answerRepository = getAnswerRepository(); | ||||||
|  |     const question = await fetchQuestion(questionId); | ||||||
|  |     const answer = await answerRepository.findAnswer(question, sequenceNumber); | ||||||
|  | 
 | ||||||
|  |     if (!answer) { | ||||||
|  |         throw new NotFoundException('Answer with questionID and sequence number not found'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return answer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getAnswer(questionId: QuestionId, sequenceNumber: number): Promise<AnswerDTO> { | ||||||
|  |     const answer = await fetchAnswer(questionId, sequenceNumber); | ||||||
|  |     return mapToAnswerDTO(answer); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteAnswer(questionId: QuestionId, sequenceNumber: number): Promise<AnswerDTO> { | ||||||
|  |     const answerRepository = getAnswerRepository(); | ||||||
|  | 
 | ||||||
|  |     const question = await fetchQuestion(questionId); | ||||||
|  |     const answer = await fetchAnswer(questionId, sequenceNumber); | ||||||
|  | 
 | ||||||
|  |     await answerRepository.removeAnswerByQuestionAndSequenceNumber(question, sequenceNumber); | ||||||
|  |     return mapToAnswerDTO(answer); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function updateAnswer(questionId: QuestionId, sequenceNumber: number, answerData: AnswerData): Promise<AnswerDTO> { | ||||||
|  |     const answerRepository = getAnswerRepository(); | ||||||
|  |     const answer = await fetchAnswer(questionId, sequenceNumber); | ||||||
|  | 
 | ||||||
|  |     const newAnswer = await answerRepository.updateContent(answer, answerData.content); | ||||||
|  |     return mapToAnswerDTO(newAnswer); | ||||||
|  | } | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import { getAttachmentRepository } from '../../data/repositories.js'; | import { getAttachmentRepository } from '../../data/repositories.js'; | ||||||
| import { Attachment } from '../../entities/content/attachment.entity.js'; | import { Attachment } from '../../entities/content/attachment.entity.js'; | ||||||
| 
 | 
 | ||||||
| import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| 
 | 
 | ||||||
| const attachmentService = { | const attachmentService = { | ||||||
|     async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> { |     async getAttachment(learningObjectId: LearningObjectIdentifierDTO, attachmentName: string): Promise<Attachment | null> { | ||||||
|         const attachmentRepo = getAttachmentRepository(); |         const attachmentRepo = getAttachmentRepository(); | ||||||
| 
 | 
 | ||||||
|         if (learningObjectId.version) { |         if (learningObjectId.version) { | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ import processingService from './processing/processing-service.js'; | ||||||
| import { NotFoundError } from '@mikro-orm/core'; | import { NotFoundError } from '@mikro-orm/core'; | ||||||
| import learningObjectService from './learning-object-service.js'; | import learningObjectService from './learning-object-service.js'; | ||||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | import { getLogger, Logger } from '../../logging/initalize.js'; | ||||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| 
 | 
 | ||||||
| const logger: Logger = getLogger(); | const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|  | @ -40,7 +40,7 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> { | async function findLearningObjectEntityById(id: LearningObjectIdentifierDTO): Promise<LearningObject | null> { | ||||||
|     const learningObjectRepo = getLearningObjectRepository(); |     const learningObjectRepo = getLearningObjectRepository(); | ||||||
| 
 | 
 | ||||||
|     return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); |     return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); | ||||||
|  | @ -53,7 +53,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | ||||||
|     /** |     /** | ||||||
|      * Fetches a single learning object by its HRUID |      * Fetches a single learning object by its HRUID | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { |     async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> { | ||||||
|         const learningObject = await findLearningObjectEntityById(id); |         const learningObject = await findLearningObjectEntityById(id); | ||||||
|         return convertLearningObject(learningObject); |         return convertLearningObject(learningObject); | ||||||
|     }, |     }, | ||||||
|  | @ -61,7 +61,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | ||||||
|     /** |     /** | ||||||
|      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). |      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { |     async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> { | ||||||
|         const learningObjectRepo = getLearningObjectRepository(); |         const learningObjectRepo = getLearningObjectRepository(); | ||||||
| 
 | 
 | ||||||
|         const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); |         const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ import { LearningObjectProvider } from './learning-object-provider.js'; | ||||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | import { getLogger, Logger } from '../../logging/initalize.js'; | ||||||
| import { | import { | ||||||
|     FilteredLearningObject, |     FilteredLearningObject, | ||||||
|     LearningObjectIdentifier, |     LearningObjectIdentifierDTO, | ||||||
|     LearningObjectMetadata, |     LearningObjectMetadata, | ||||||
|     LearningObjectNode, |     LearningObjectNode, | ||||||
|     LearningPathIdentifier, |     LearningPathIdentifier, | ||||||
|  | @ -67,7 +67,7 @@ async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full | ||||||
| 
 | 
 | ||||||
|         const objects = await Promise.all( |         const objects = await Promise.all( | ||||||
|             nodes.map(async (node) => { |             nodes.map(async (node) => { | ||||||
|                 const learningObjectId: LearningObjectIdentifier = { |                 const learningObjectId: LearningObjectIdentifierDTO = { | ||||||
|                     hruid: node.learningobject_hruid, |                     hruid: node.learningobject_hruid, | ||||||
|                     language: learningPathId.language, |                     language: learningPathId.language, | ||||||
|                 }; |                 }; | ||||||
|  | @ -85,7 +85,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | ||||||
|     /** |     /** | ||||||
|      * Fetches a single learning object by its HRUID |      * Fetches a single learning object by its HRUID | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { |     async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> { | ||||||
|         const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; |         const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; | ||||||
|         const metadata = await fetchWithLogging<LearningObjectMetadata>( |         const metadata = await fetchWithLogging<LearningObjectMetadata>( | ||||||
|             metadataUrl, |             metadataUrl, | ||||||
|  | @ -121,7 +121,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | ||||||
|      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). For learning objects |      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). For learning objects | ||||||
|      * from the Dwengo API, this means passing through the HTML rendering from there. |      * from the Dwengo API, this means passing through the HTML rendering from there. | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { |     async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> { | ||||||
|         const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; |         const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; | ||||||
|         const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { |         const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { | ||||||
|             params: { ...id }, |             params: { ...id }, | ||||||
|  |  | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| 
 | 
 | ||||||
| export interface LearningObjectProvider { | export interface LearningObjectProvider { | ||||||
|     /** |     /** | ||||||
|      * Fetches a single learning object by its HRUID |      * Fetches a single learning object by its HRUID | ||||||
|      */ |      */ | ||||||
|     getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null>; |     getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null>; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Fetch full learning object data (metadata) |      * Fetch full learning object data (metadata) | ||||||
|  | @ -19,5 +19,5 @@ export interface LearningObjectProvider { | ||||||
|     /** |     /** | ||||||
|      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). |      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). | ||||||
|      */ |      */ | ||||||
|     getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null>; |     getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null>; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,9 +2,9 @@ import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provid | ||||||
| import { LearningObjectProvider } from './learning-object-provider.js'; | import { LearningObjectProvider } from './learning-object-provider.js'; | ||||||
| import { envVars, getEnvVar } from '../../util/envVars.js'; | import { envVars, getEnvVar } from '../../util/envVars.js'; | ||||||
| import databaseLearningObjectProvider from './database-learning-object-provider.js'; | import databaseLearningObjectProvider from './database-learning-object-provider.js'; | ||||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| 
 | 
 | ||||||
| function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { | function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { | ||||||
|     if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { |     if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { | ||||||
|         return databaseLearningObjectProvider; |         return databaseLearningObjectProvider; | ||||||
|     } |     } | ||||||
|  | @ -18,7 +18,7 @@ const learningObjectService = { | ||||||
|     /** |     /** | ||||||
|      * Fetches a single learning object by its HRUID |      * Fetches a single learning object by its HRUID | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { |     async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> { | ||||||
|         return getProvider(id).getLearningObjectById(id); |         return getProvider(id).getLearningObjectById(id); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|  | @ -39,7 +39,7 @@ const learningObjectService = { | ||||||
|     /** |     /** | ||||||
|      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). |      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { |     async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> { | ||||||
|         return getProvider(id).getLearningObjectHTML(id); |         return getProvider(id).getLearningObjectHTML(id); | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ import Image = marked.Tokens.Image; | ||||||
| import Heading = marked.Tokens.Heading; | import Heading = marked.Tokens.Heading; | ||||||
| import Link = marked.Tokens.Link; | import Link = marked.Tokens.Link; | ||||||
| import RendererObject = marked.RendererObject; | import RendererObject = marked.RendererObject; | ||||||
| import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| import { Language } from '@dwengo-1/common/util/language'; | import { Language } from '@dwengo-1/common/util/language'; | ||||||
| 
 | 
 | ||||||
| const prefixes = { | const prefixes = { | ||||||
|  | @ -25,7 +25,7 @@ const prefixes = { | ||||||
|     blockly: '@blockly', |     blockly: '@blockly', | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifier { | function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifierDTO { | ||||||
|     const [hruid, language, version] = href.split(/\/(.+)/, 2)[1].split('/'); |     const [hruid, language, version] = href.split(/\/(.+)/, 2)[1].split('/'); | ||||||
|     return { |     return { | ||||||
|         hruid, |         hruid, | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ import { LearningObject } from '../../../entities/content/learning-object.entity | ||||||
| import Processor from './processor.js'; | import Processor from './processor.js'; | ||||||
| import { DwengoContentType } from './content-type.js'; | import { DwengoContentType } from './content-type.js'; | ||||||
| import { replaceAsync } from '../../../util/async.js'; | import { replaceAsync } from '../../../util/async.js'; | ||||||
| import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| import { Language } from '@dwengo-1/common/util/language'; | import { Language } from '@dwengo-1/common/util/language'; | ||||||
| 
 | 
 | ||||||
| const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" language="([^"]+)" version="([^"]+)"\/>/g; | const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" language="([^"]+)" version="([^"]+)"\/>/g; | ||||||
|  | @ -50,7 +50,7 @@ class ProcessingService { | ||||||
|      */ |      */ | ||||||
|     async render( |     async render( | ||||||
|         learningObject: LearningObject, |         learningObject: LearningObject, | ||||||
|         fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifier) => Promise<LearningObject | null> |         fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifierDTO) => Promise<LearningObject | null> | ||||||
|     ): Promise<string> { |     ): Promise<string> { | ||||||
|         const html = this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject); |         const html = this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject); | ||||||
|         if (fetchEmbeddedLearningObjects) { |         if (fetchEmbeddedLearningObjects) { | ||||||
|  |  | ||||||
|  | @ -1,22 +1,17 @@ | ||||||
| import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js'; | import { getQuestionRepository } from '../data/repositories.js'; | ||||||
| import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; | import { mapToLearningObjectID, mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; | ||||||
| import { Question } from '../entities/questions/question.entity.js'; | import { Question } from '../entities/questions/question.entity.js'; | ||||||
| import { Answer } from '../entities/questions/answer.entity.js'; |  | ||||||
| import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js'; |  | ||||||
| import { QuestionRepository } from '../data/questions/question-repository.js'; | import { QuestionRepository } from '../data/questions/question-repository.js'; | ||||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
| import { mapToStudent } from '../interfaces/student.js'; | import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||||
| import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
| import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; | import { FALLBACK_VERSION_NUM } from '../config.js'; | ||||||
|  | import { fetchStudent } from './students.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> { | export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> { | ||||||
|     const questionRepository: QuestionRepository = getQuestionRepository(); |     const questionRepository: QuestionRepository = getQuestionRepository(); | ||||||
|     const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); |     const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); | ||||||
| 
 | 
 | ||||||
|     if (!questions) { |  | ||||||
|         return []; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (full) { |     if (full) { | ||||||
|         return questions.map(mapToQuestionDTO); |         return questions.map(mapToQuestionDTO); | ||||||
|     } |     } | ||||||
|  | @ -24,90 +19,57 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea | ||||||
|     return questions.map(mapToQuestionDTOId); |     return questions.map(mapToQuestionDTOId); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function fetchQuestion(questionId: QuestionId): Promise<Question | null> { | export async function fetchQuestion(questionId: QuestionId): Promise<Question> { | ||||||
|     const questionRepository = getQuestionRepository(); |     const questionRepository = getQuestionRepository(); | ||||||
|  |     const question = await questionRepository.findByLearningObjectAndSequenceNumber( | ||||||
|  |         mapToLearningObjectID(questionId.learningObjectIdentifier), | ||||||
|  |         questionId.sequenceNumber | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     return await questionRepository.findOne({ |     if (!question) { | ||||||
|         learningObjectHruid: questionId.learningObjectIdentifier.hruid, |         throw new NotFoundException('Question with loID and sequence number not found'); | ||||||
|         learningObjectLanguage: questionId.learningObjectIdentifier.language, |     } | ||||||
|         learningObjectVersion: questionId.learningObjectIdentifier.version, | 
 | ||||||
|         sequenceNumber: questionId.sequenceNumber, |     return question; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO> { | ||||||
|  |     const question = await fetchQuestion(questionId); | ||||||
|  |     return mapToQuestionDTO(question); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createQuestion(loId: LearningObjectIdentifier, questionData: QuestionData): Promise<QuestionDTO> { | ||||||
|  |     const questionRepository = getQuestionRepository(); | ||||||
|  |     const author = await fetchStudent(questionData.author!); | ||||||
|  |     const content = questionData.content; | ||||||
|  | 
 | ||||||
|  |     const question = await questionRepository.createQuestion({ | ||||||
|  |         loId, | ||||||
|  |         author, | ||||||
|  |         content, | ||||||
|     }); |     }); | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO | null> { |  | ||||||
|     const question = await fetchQuestion(questionId); |  | ||||||
| 
 |  | ||||||
|     if (!question) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     return mapToQuestionDTO(question); |     return mapToQuestionDTO(question); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise<AnswerDTO[] | AnswerId[]> { | export async function deleteQuestion(questionId: QuestionId): Promise<QuestionDTO> { | ||||||
|     const answerRepository = getAnswerRepository(); |  | ||||||
|     const question = await fetchQuestion(questionId); |  | ||||||
| 
 |  | ||||||
|     if (!question) { |  | ||||||
|         return []; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question); |  | ||||||
| 
 |  | ||||||
|     if (!answers) { |  | ||||||
|         return []; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (full) { |  | ||||||
|         return answers.map(mapToAnswerDTO); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return answers.map(mapToAnswerDTOId); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function createQuestion(questionDTO: QuestionDTO): Promise<QuestionDTO | null> { |  | ||||||
|     const questionRepository = getQuestionRepository(); |     const questionRepository = getQuestionRepository(); | ||||||
| 
 |     const question = await fetchQuestion(questionId); // Throws error if not found
 | ||||||
|     const author = mapToStudent(questionDTO.author); |  | ||||||
| 
 | 
 | ||||||
|     const loId: LearningObjectIdentifier = { |     const loId: LearningObjectIdentifier = { | ||||||
|         ...questionDTO.learningObjectIdentifier, |         hruid: questionId.learningObjectIdentifier.hruid, | ||||||
|         version: questionDTO.learningObjectIdentifier.version ?? 1, |         language: questionId.learningObjectIdentifier.language, | ||||||
|  |         version: questionId.learningObjectIdentifier.version || FALLBACK_VERSION_NUM, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     try { |     await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(loId, questionId.sequenceNumber); | ||||||
|         await questionRepository.createQuestion({ |  | ||||||
|             loId, |  | ||||||
|             author, |  | ||||||
|             content: questionDTO.content, |  | ||||||
|         }); |  | ||||||
|     } catch (_) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return questionDTO; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function deleteQuestion(questionId: QuestionId): Promise<QuestionDTO | null> { |  | ||||||
|     const questionRepository = getQuestionRepository(); |  | ||||||
| 
 |  | ||||||
|     const question = await fetchQuestion(questionId); |  | ||||||
| 
 |  | ||||||
|     if (!question) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const loId: LearningObjectIdentifier = { |  | ||||||
|         ...questionId.learningObjectIdentifier, |  | ||||||
|         version: questionId.learningObjectIdentifier.version ?? 1, |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
|         await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(loId, questionId.sequenceNumber); |  | ||||||
|     } catch (_) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return mapToQuestionDTO(question); |     return mapToQuestionDTO(question); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export async function updateQuestion(questionId: QuestionId, questionData: QuestionData): Promise<QuestionDTO> { | ||||||
|  |     const questionRepository = getQuestionRepository(); | ||||||
|  |     const question = await fetchQuestion(questionId); | ||||||
|  | 
 | ||||||
|  |     const newQuestion = await questionRepository.updateContent(question, questionData.content); | ||||||
|  |     return mapToQuestionDTO(newQuestion); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| 
 | 
 | ||||||
| export function isValidHttpUrl(url: string): boolean { | export function isValidHttpUrl(url: string): boolean { | ||||||
|     try { |     try { | ||||||
|  | @ -9,7 +9,7 @@ export function isValidHttpUrl(url: string): boolean { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifier): string { | export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifierDTO): string { | ||||||
|     let url = `/learningObject/${learningObjectId.hruid}/html?language=${learningObjectId.language}`; |     let url = `/learningObject/${learningObjectId.hruid}/html?language=${learningObjectId.language}`; | ||||||
|     if (learningObjectId.version) { |     if (learningObjectId.version) { | ||||||
|         url += `&version=${learningObjectId.version}`; |         url += `&version=${learningObjectId.version}`; | ||||||
|  | @ -17,7 +17,7 @@ export function getUrlStringForLearningObject(learningObjectId: LearningObjectId | ||||||
|     return url; |     return url; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getUrlStringForLearningObjectHTML(learningObjectIdentifier: LearningObjectIdentifier): string { | export function getUrlStringForLearningObjectHTML(learningObjectIdentifier: LearningObjectIdentifierDTO): string { | ||||||
|     let url = `/learningObject/${learningObjectIdentifier.hruid}/html?language=${learningObjectIdentifier.language}`; |     let url = `/learningObject/${learningObjectIdentifier.hruid}/html?language=${learningObjectIdentifier.language}`; | ||||||
|     if (learningObjectIdentifier.version) { |     if (learningObjectIdentifier.version) { | ||||||
|         url += `&version=${learningObjectIdentifier.version}`; |         url += `&version=${learningObjectIdentifier.version}`; | ||||||
|  |  | ||||||
							
								
								
									
										87
									
								
								backend/tests/controllers/answers.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								backend/tests/controllers/answers.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,87 @@ | ||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; | ||||||
|  | import { setupTestApp } from '../setup-tests'; | ||||||
|  | import { Language } from '@dwengo-1/common/util/language'; | ||||||
|  | import { getAllAnswersHandler, getAnswerHandler, updateAnswerHandler } from '../../src/controllers/answers'; | ||||||
|  | import { BadRequestException } from '../../src/exceptions/bad-request-exception'; | ||||||
|  | import { NotFoundException } from '../../src/exceptions/not-found-exception'; | ||||||
|  | 
 | ||||||
|  | describe('Questions controllers', () => { | ||||||
|  |     let req: Partial<Request>; | ||||||
|  |     let res: Partial<Response>; | ||||||
|  | 
 | ||||||
|  |     let jsonMock: Mock; | ||||||
|  | 
 | ||||||
|  |     beforeAll(async () => { | ||||||
|  |         await setupTestApp(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     beforeEach(() => { | ||||||
|  |         jsonMock = vi.fn(); | ||||||
|  |         res = { | ||||||
|  |             json: jsonMock, | ||||||
|  |         }; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get answers list', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { hruid: 'id05', version: '1', seq: '2' }, | ||||||
|  |             query: { lang: Language.English, full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getAllAnswersHandler(req as Request, res as Response); | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ answers: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         // Console.log(result.answers);
 | ||||||
|  |         expect(result.answers).to.have.length.greaterThan(1); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get answer', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { hruid: 'id05', version: '1', seq: '2', seqAnswer: '2' }, | ||||||
|  |             query: { lang: Language.English, full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getAnswerHandler(req as Request, res as Response); | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ answer: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         // Const result = jsonMock.mock.lastCall?.[0];
 | ||||||
|  |         // Console.log(result.answer);
 | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get answer hruid does not exist', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { hruid: 'id_not_exist' }, | ||||||
|  |             query: { lang: Language.English, full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => getAnswerHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get answer no hruid given', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: {}, | ||||||
|  |             query: { lang: Language.English, full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => getAnswerHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Update question', async () => { | ||||||
|  |         const newContent = 'updated question'; | ||||||
|  |         req = { | ||||||
|  |             params: { hruid: 'id05', version: '1', seq: '2', seqAnswer: '2' }, | ||||||
|  |             query: { lang: Language.English }, | ||||||
|  |             body: { content: newContent }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await updateAnswerHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ answer: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         // Console.log(result.question);
 | ||||||
|  |         expect(result.answer.content).to.eq(newContent); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
							
								
								
									
										117
									
								
								backend/tests/controllers/questions.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								backend/tests/controllers/questions.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,117 @@ | ||||||
|  | import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; | ||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import { setupTestApp } from '../setup-tests'; | ||||||
|  | import { getAllQuestionsHandler, getQuestionHandler, updateQuestionHandler } from '../../src/controllers/questions'; | ||||||
|  | import { Language } from '@dwengo-1/common/util/language'; | ||||||
|  | import { NotFoundException } from '../../src/exceptions/not-found-exception'; | ||||||
|  | import { BadRequestException } from '../../src/exceptions/bad-request-exception'; | ||||||
|  | 
 | ||||||
|  | describe('Questions controllers', () => { | ||||||
|  |     let req: Partial<Request>; | ||||||
|  |     let res: Partial<Response>; | ||||||
|  | 
 | ||||||
|  |     let jsonMock: Mock; | ||||||
|  | 
 | ||||||
|  |     beforeAll(async () => { | ||||||
|  |         await setupTestApp(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     beforeEach(() => { | ||||||
|  |         jsonMock = vi.fn(); | ||||||
|  |         res = { | ||||||
|  |             json: jsonMock, | ||||||
|  |         }; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get question list', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { hruid: 'id05', version: '1' }, | ||||||
|  |             query: { lang: Language.English, full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getAllQuestionsHandler(req as Request, res as Response); | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ questions: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         // Console.log(result.questions);
 | ||||||
|  |         expect(result.questions).to.have.length.greaterThan(1); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get question', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { hruid: 'id05', version: '1', seq: '1' }, | ||||||
|  |             query: { lang: Language.English, full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getQuestionHandler(req as Request, res as Response); | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         // Const result = jsonMock.mock.lastCall?.[0];
 | ||||||
|  |         // Console.log(result.question);
 | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get question with fallback sequence number and version', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { hruid: 'id05' }, | ||||||
|  |             query: { lang: Language.English, full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await getQuestionHandler(req as Request, res as Response); | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         // Const result = jsonMock.mock.lastCall?.[0];
 | ||||||
|  |         // Console.log(result.question);
 | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get question hruid does not exist', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: { hruid: 'id_not_exist' }, | ||||||
|  |             query: { lang: Language.English, full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => getQuestionHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Get question no hruid given', async () => { | ||||||
|  |         req = { | ||||||
|  |             params: {}, | ||||||
|  |             query: { lang: Language.English, full: 'true' }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await expect(async () => getQuestionHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |     It('Create and delete question', async() => { | ||||||
|  |         req = { | ||||||
|  |             params: { hruid: 'id05', version: '1', seq: '2'}, | ||||||
|  |             query: { lang: Language.English }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await deleteQuestionHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         console.log(result.question); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |      */ | ||||||
|  | 
 | ||||||
|  |     it('Update question', async () => { | ||||||
|  |         const newContent = 'updated question'; | ||||||
|  |         req = { | ||||||
|  |             params: { hruid: 'id05', version: '1', seq: '1' }, | ||||||
|  |             query: { lang: Language.English }, | ||||||
|  |             body: { content: newContent }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await updateQuestionHandler(req as Request, res as Response); | ||||||
|  | 
 | ||||||
|  |         expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); | ||||||
|  | 
 | ||||||
|  |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
|  |         // Console.log(result.question);
 | ||||||
|  |         expect(result.question.content).to.eq(newContent); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | @ -7,11 +7,11 @@ import learningObjectService from '../../../src/services/learning-objects/learni | ||||||
| import { envVars, getEnvVar } from '../../../src/util/envVars'; | import { envVars, getEnvVar } from '../../../src/util/envVars'; | ||||||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity'; | import { LearningPath } from '../../../src/entities/content/learning-path.entity'; | ||||||
| import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; | import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; | ||||||
| import { LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | import { LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| import { Language } from '@dwengo-1/common/util/language'; | import { Language } from '@dwengo-1/common/util/language'; | ||||||
| 
 | 
 | ||||||
| const EXPECTED_DWENGO_LEARNING_OBJECT_TITLE = 'Werken met notebooks'; | const EXPECTED_DWENGO_LEARNING_OBJECT_TITLE = 'Werken met notebooks'; | ||||||
| const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifier = { | const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifierDTO = { | ||||||
|     hruid: 'pn_werkingnotebooks', |     hruid: 'pn_werkingnotebooks', | ||||||
|     language: Language.Dutch, |     language: Language.Dutch, | ||||||
|     version: 3, |     version: 3, | ||||||
|  |  | ||||||
|  | @ -1,14 +1,19 @@ | ||||||
| import { UserDTO } from './user'; |  | ||||||
| import { QuestionDTO, QuestionId } from './question'; | import { QuestionDTO, QuestionId } from './question'; | ||||||
|  | import { TeacherDTO } from './teacher'; | ||||||
| 
 | 
 | ||||||
| export interface AnswerDTO { | export interface AnswerDTO { | ||||||
|     author: UserDTO; |     author: TeacherDTO; | ||||||
|     toQuestion: QuestionDTO; |     toQuestion: QuestionDTO; | ||||||
|     sequenceNumber: number; |     sequenceNumber: number; | ||||||
|     timestamp: string; |     timestamp: string; | ||||||
|     content: string; |     content: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface AnswerData { | ||||||
|  |     author: string; | ||||||
|  |     content: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export interface AnswerId { | export interface AnswerId { | ||||||
|     author: string; |     author: string; | ||||||
|     toQuestion: QuestionId; |     toQuestion: QuestionId; | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ export interface Transition { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface LearningObjectIdentifier { | export interface LearningObjectIdentifierDTO { | ||||||
|     hruid: string; |     hruid: string; | ||||||
|     language: Language; |     language: Language; | ||||||
|     version?: number; |     version?: number; | ||||||
|  |  | ||||||
|  | @ -1,15 +1,20 @@ | ||||||
| import { LearningObjectIdentifier } from './learning-content'; | import { LearningObjectIdentifierDTO } from './learning-content'; | ||||||
| import { StudentDTO } from './student'; | import { StudentDTO } from './student'; | ||||||
| 
 | 
 | ||||||
| export interface QuestionDTO { | export interface QuestionDTO { | ||||||
|     learningObjectIdentifier: LearningObjectIdentifier; |     learningObjectIdentifier: LearningObjectIdentifierDTO; | ||||||
|     sequenceNumber?: number; |     sequenceNumber?: number; | ||||||
|     author: StudentDTO; |     author: StudentDTO; | ||||||
|     timestamp?: string; |     timestamp: string; | ||||||
|  |     content: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface QuestionData { | ||||||
|  |     author?: string; | ||||||
|     content: string; |     content: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface QuestionId { | export interface QuestionId { | ||||||
|     learningObjectIdentifier: LearningObjectIdentifier; |     learningObjectIdentifier: LearningObjectIdentifierDTO; | ||||||
|     sequenceNumber: number; |     sequenceNumber: number; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import { GroupDTO } from './group'; | import { GroupDTO } from './group'; | ||||||
| import { LearningObjectIdentifier } from './learning-content'; | import { LearningObjectIdentifierDTO } from './learning-content'; | ||||||
| import { StudentDTO } from './student'; | import { StudentDTO } from './student'; | ||||||
| import { Language } from '../util/language'; | import { Language } from '../util/language'; | ||||||
| 
 | 
 | ||||||
| export interface SubmissionDTO { | export interface SubmissionDTO { | ||||||
|     learningObjectIdentifier: LearningObjectIdentifier; |     learningObjectIdentifier: LearningObjectIdentifierDTO; | ||||||
| 
 | 
 | ||||||
|     submissionNumber?: number; |     submissionNumber?: number; | ||||||
|     submitter: StudentDTO; |     submitter: StudentDTO; | ||||||
|  |  | ||||||
							
								
								
									
										39
									
								
								frontend/src/controllers/answers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								frontend/src/controllers/answers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | ||||||
|  | import type { AnswerData, AnswerDTO, AnswerId } from "@dwengo-1/common/interfaces/answer"; | ||||||
|  | import { BaseController } from "@/controllers/base-controller.ts"; | ||||||
|  | import type { QuestionId } from "@dwengo-1/common/interfaces/question"; | ||||||
|  | 
 | ||||||
|  | export interface AnswersResponse { | ||||||
|  |     answers: AnswerDTO[] | AnswerId[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface AnswerResponse { | ||||||
|  |     answer: AnswerDTO; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class AnswerController extends BaseController { | ||||||
|  |     constructor(questionId: QuestionId) { | ||||||
|  |         this.loId = questionId.learningObjectIdentifier; | ||||||
|  |         this.sequenceNumber = questionId.sequenceNumber; | ||||||
|  |         super(`learningObject/${loId.hruid}/:${loId.version}/questions/${this.sequenceNumber}/answers`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getAll(full = true): Promise<AnswersResponse> { | ||||||
|  |         return this.get<AnswersResponse>("/", { lang: this.loId.lang, full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getBy(seq: number): Promise<AnswerResponse> { | ||||||
|  |         return this.get<AnswerResponse>(`/${seq}`, { lang: this.loId.lang }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async create(answerData: AnswerData): Promise<AnswerResponse> { | ||||||
|  |         return this.post<AnswerResponse>("/", answerData, { lang: this.loId.lang }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async remove(seq: number): Promise<AnswerResponse> { | ||||||
|  |         return this.delete<AnswerResponse>(`/${seq}`, { lang: this.loId.lang }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async update(seq: number, answerData: AnswerData): Promise<AnswerResponse> { | ||||||
|  |         return this.put<AnswerResponse>(`/${seq}`, answerData, { lang: this.loId.lang }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -21,20 +21,20 @@ export abstract class BaseController { | ||||||
|         return response.data; |         return response.data; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected async post<T>(path: string, body: unknown): Promise<T> { |     protected async post<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> { | ||||||
|         const response = await apiClient.post<T>(this.absolutePathFor(path), body); |         const response = await apiClient.post<T>(this.absolutePathFor(path), body, { params: queryParams }); | ||||||
|         BaseController.assertSuccessResponse(response); |         BaseController.assertSuccessResponse(response); | ||||||
|         return response.data; |         return response.data; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected async delete<T>(path: string): Promise<T> { |     protected async delete<T>(path: string, queryParams?: QueryParams): Promise<T> { | ||||||
|         const response = await apiClient.delete<T>(this.absolutePathFor(path)); |         const response = await apiClient.delete<T>(this.absolutePathFor(path), { params: queryParams }); | ||||||
|         BaseController.assertSuccessResponse(response); |         BaseController.assertSuccessResponse(response); | ||||||
|         return response.data; |         return response.data; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected async put<T>(path: string, body: unknown): Promise<T> { |     protected async put<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> { | ||||||
|         const response = await apiClient.put<T>(this.absolutePathFor(path), body); |         const response = await apiClient.put<T>(this.absolutePathFor(path), body, { params: queryParams }); | ||||||
|         BaseController.assertSuccessResponse(response); |         BaseController.assertSuccessResponse(response); | ||||||
|         return response.data; |         return response.data; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,38 @@ | ||||||
| import type { QuestionDTO, QuestionId } from "@dwengo-1/common/interfaces/question"; | import type { QuestionData, QuestionDTO, QuestionId } from "@dwengo-1/common/interfaces/question"; | ||||||
|  | import { BaseController } from "@/controllers/base-controller.ts"; | ||||||
|  | import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||||
| 
 | 
 | ||||||
| export interface QuestionsResponse { | export interface QuestionsResponse { | ||||||
|     questions: QuestionDTO[] | QuestionId[]; |     questions: QuestionDTO[] | QuestionId[]; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export interface QuestionResponse { | ||||||
|  |     question: QuestionDTO; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class QuestionController extends BaseController { | ||||||
|  |     constructor(loId: LearningObjectIdentifierDTO) { | ||||||
|  |         this.loId = loId; | ||||||
|  |         super(`learningObject/${loId.hruid}/:${loId.version}/questions`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getAll(full = true): Promise<QuestionsResponse> { | ||||||
|  |         return this.get<QuestionsResponse>("/", { lang: this.loId.lang, full }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async getBy(sequenceNumber: number): Promise<QuestionResponse> { | ||||||
|  |         return this.get<QuestionResponse>(`/${sequenceNumber}`, { lang: this.loId.lang }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async create(questionData: QuestionData): Promise<QuestionResponse> { | ||||||
|  |         return this.post<QuestionResponse>("/", questionData, { lang: this.loId.lang }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async remove(sequenceNumber: number): Promise<QuestionResponse> { | ||||||
|  |         return this.delete<QuestionResponse>(`/${sequenceNumber}`, { lang: this.loId.lang }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async update(sequenceNumber: number, questionData: QuestionData): Promise<QuestionResponse> { | ||||||
|  |         return this.put<QuestionResponse>(`/${sequenceNumber}`, questionData, { lang: this.loId.lang }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										51
									
								
								frontend/src/queries/answers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								frontend/src/queries/answers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | ||||||
|  | import type { QuestionId } from "@dwengo-1/common/dist/interfaces/question.ts"; | ||||||
|  | import { type MaybeRefOrGetter, toValue } from "vue"; | ||||||
|  | import { useMutation, type UseMutationReturnType, useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||||
|  | import { AnswerController, type AnswerResponse, type AnswersResponse } from "@/controllers/answers.ts"; | ||||||
|  | import type { AnswerData } from "@dwengo-1/common/dist/interfaces/answer.ts"; | ||||||
|  | 
 | ||||||
|  | // TODO caching
 | ||||||
|  | 
 | ||||||
|  | export function useAnswersQuery( | ||||||
|  |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = true, | ||||||
|  | ): UseQueryReturnType<AnswersResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryFn: async () => new AnswerController(toValue(questionId)).getAll(toValue(full)), | ||||||
|  |         enabled: () => Boolean(toValue(questionId)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useAnswerQuery( | ||||||
|  |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
|  |     sequenceNumber: MaybeRefOrGetter<number>, | ||||||
|  | ): UseQueryReturnType<AnswerResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryFn: async () => new AnswerController(toValue(questionId)).getBy(toValue(sequenceNumber)), | ||||||
|  |         enabled: () => Boolean(toValue(questionId)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useCreateAnswerMutation( | ||||||
|  |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
|  | ): UseMutationReturnType<AnswerResponse, Error, AnswerData, unknown> { | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async (data) => new AnswerController(toValue(questionId)).create(data), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useDeleteAnswerMutation( | ||||||
|  |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
|  | ): UseMutationReturnType<AnswerResponse, Error, number, unknown> { | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async (seq) => new AnswerController(toValue(questionId)).remove(seq), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useUpdateAnswerMutation( | ||||||
|  |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
|  | ): UseMutationReturnType<AnswerResponse, Error, { answerData: AnswerData; seq: number }, unknown> { | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async (data, seq) => new AnswerController(toValue(questionId)).update(seq, data), | ||||||
|  |     }); | ||||||
|  | } | ||||||
							
								
								
									
										93
									
								
								frontend/src/queries/questions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								frontend/src/queries/questions.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,93 @@ | ||||||
|  | import { QuestionController, type QuestionResponse, type QuestionsResponse } from "@/controllers/questions.ts"; | ||||||
|  | import type { QuestionData, QuestionId } from "@dwengo-1/common/interfaces/question"; | ||||||
|  | import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||||
|  | import { computed, type MaybeRefOrGetter, toValue } from "vue"; | ||||||
|  | import { | ||||||
|  |     useMutation, | ||||||
|  |     type UseMutationReturnType, | ||||||
|  |     useQuery, | ||||||
|  |     useQueryClient, | ||||||
|  |     type UseQueryReturnType, | ||||||
|  | } from "@tanstack/vue-query"; | ||||||
|  | 
 | ||||||
|  | export function questionsQueryKey( | ||||||
|  |     loId: LearningObjectIdentifierDTO, | ||||||
|  |     full: boolean, | ||||||
|  | ): [string, string, number, string, boolean] { | ||||||
|  |     return ["questions", loId.hruid, loId.version, loId.language, full]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function questionQueryKey(questionId: QuestionId): [string, string, number, string, number] { | ||||||
|  |     const loId = questionId.learningObjectIdentifier; | ||||||
|  |     return ["question", loId.hruid, loId.version, loId.language, questionId.sequenceNumber]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useQuestionsQuery( | ||||||
|  |     loId: MaybeRefOrGetter<LearningObjectIdentifierDTO>, | ||||||
|  |     full: MaybeRefOrGetter<boolean> = true, | ||||||
|  | ): UseQueryReturnType<QuestionsResponse, Error> { | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => questionsQueryKey(toValue(loId), toValue(full))), | ||||||
|  |         queryFn: async () => new QuestionController(toValue(loId)).getAll(toValue(full)), | ||||||
|  |         enabled: () => Boolean(toValue(loId)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useQuestionQuery( | ||||||
|  |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
|  | ): UseQueryReturnType<QuestionResponse, Error> { | ||||||
|  |     const loId = toValue(questionId).learningObjectIdentifier; | ||||||
|  |     const sequenceNumber = toValue(questionId).sequenceNumber; | ||||||
|  |     return useQuery({ | ||||||
|  |         queryKey: computed(() => questionQueryKey(loId, sequenceNumber)), | ||||||
|  |         queryFn: async () => new QuestionController(loId).getBy(sequenceNumber), | ||||||
|  |         enabled: () => Boolean(toValue(questionId)), | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useCreateQuestionMutation( | ||||||
|  |     loId: MaybeRefOrGetter<LearningObjectIdentifierDTO>, | ||||||
|  | ): UseMutationReturnType<QuestionResponse, Error, QuestionData, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async (data) => new QuestionController(toValue(loId)).create(data), | ||||||
|  |         onSuccess: async () => { | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useUpdateQuestionMutation( | ||||||
|  |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
|  | ): UseMutationReturnType<QuestionResponse, Error, QuestionData, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  |     const loId = toValue(questionId).learningObjectIdentifier; | ||||||
|  |     const sequenceNumber = toValue(questionId).sequenceNumber; | ||||||
|  | 
 | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async (data) => new QuestionController(loId).update(sequenceNumber, data), | ||||||
|  |         onSuccess: async () => { | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useDeleteQuestionMutation( | ||||||
|  |     questionId: MaybeRefOrGetter<QuestionId>, | ||||||
|  | ): UseMutationReturnType<QuestionResponse, Error, void, unknown> { | ||||||
|  |     const queryClient = useQueryClient(); | ||||||
|  |     const loId = toValue(questionId).learningObjectIdentifier; | ||||||
|  |     const sequenceNumber = toValue(questionId).sequenceNumber; | ||||||
|  |     return useMutation({ | ||||||
|  |         mutationFn: async () => new QuestionController(loId).remove(sequenceNumber), | ||||||
|  |         onSuccess: async () => { | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl