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)) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue