Merge remote-tracking branch 'origin/dev' into fix/class-join-request
# Conflicts: # backend/src/controllers/classes.ts # backend/src/data/questions/question-repository.ts # backend/tests/controllers/students.test.ts # backend/tests/controllers/teachers.test.ts
This commit is contained in:
commit
0a0857deb9
58 changed files with 2058 additions and 277 deletions
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 { 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 = {
|
||||
async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
|
||||
async getAttachment(learningObjectId: LearningObjectIdentifierDTO, attachmentName: string): Promise<Attachment | null> {
|
||||
const attachmentRepo = getAttachmentRepository();
|
||||
|
||||
if (learningObjectId.version) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import processingService from './processing/processing-service.js';
|
|||
import { NotFoundError } from '@mikro-orm/core';
|
||||
import learningObjectService from './learning-object-service.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();
|
||||
|
||||
|
@ -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();
|
||||
|
||||
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
|
||||
|
@ -53,7 +53,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
|
|||
/**
|
||||
* 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);
|
||||
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).
|
||||
*/
|
||||
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
|
||||
async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> {
|
||||
const learningObjectRepo = getLearningObjectRepository();
|
||||
|
||||
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 {
|
||||
FilteredLearningObject,
|
||||
LearningObjectIdentifier,
|
||||
LearningObjectIdentifierDTO,
|
||||
LearningObjectMetadata,
|
||||
LearningObjectNode,
|
||||
LearningPathIdentifier,
|
||||
|
@ -67,7 +67,7 @@ async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full
|
|||
|
||||
const objects = await Promise.all(
|
||||
nodes.map(async (node) => {
|
||||
const learningObjectId: LearningObjectIdentifier = {
|
||||
const learningObjectId: LearningObjectIdentifierDTO = {
|
||||
hruid: node.learningobject_hruid,
|
||||
language: learningPathId.language,
|
||||
};
|
||||
|
@ -85,7 +85,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
|
|||
/**
|
||||
* 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 metadata = await fetchWithLogging<LearningObjectMetadata>(
|
||||
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
|
||||
* 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 html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, {
|
||||
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 {
|
||||
/**
|
||||
* 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)
|
||||
|
@ -19,5 +19,5 @@ export interface LearningObjectProvider {
|
|||
/**
|
||||
* 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 { envVars, getEnvVar } from '../../util/envVars.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))) {
|
||||
return databaseLearningObjectProvider;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ const learningObjectService = {
|
|||
/**
|
||||
* 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);
|
||||
},
|
||||
|
||||
|
@ -39,7 +39,7 @@ const learningObjectService = {
|
|||
/**
|
||||
* 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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import Image = marked.Tokens.Image;
|
|||
import Heading = marked.Tokens.Heading;
|
||||
import Link = marked.Tokens.Link;
|
||||
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';
|
||||
|
||||
const prefixes = {
|
||||
|
@ -25,7 +25,7 @@ const prefixes = {
|
|||
blockly: '@blockly',
|
||||
};
|
||||
|
||||
function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifier {
|
||||
function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifierDTO {
|
||||
const [hruid, language, version] = href.split(/\/(.+)/, 2)[1].split('/');
|
||||
return {
|
||||
hruid,
|
||||
|
|
|
@ -14,7 +14,7 @@ import { LearningObject } from '../../../entities/content/learning-object.entity
|
|||
import Processor from './processor.js';
|
||||
import { DwengoContentType } from './content-type.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';
|
||||
|
||||
const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" language="([^"]+)" version="([^"]+)"\/>/g;
|
||||
|
@ -50,7 +50,7 @@ class ProcessingService {
|
|||
*/
|
||||
async render(
|
||||
learningObject: LearningObject,
|
||||
fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifier) => Promise<LearningObject | null>
|
||||
fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifierDTO) => Promise<LearningObject | null>
|
||||
): Promise<string> {
|
||||
const html = this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject);
|
||||
if (fetchEmbeddedLearningObjects) {
|
||||
|
|
|
@ -1,22 +1,17 @@
|
|||
import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js';
|
||||
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
|
||||
import { getQuestionRepository } from '../data/repositories.js';
|
||||
import { mapToLearningObjectID, mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.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 { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||
import { mapToStudent } from '../interfaces/student.js';
|
||||
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||
import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
|
||||
import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||
import { FALLBACK_VERSION_NUM } from '../config.js';
|
||||
import { fetchStudent } from './students.js';
|
||||
|
||||
export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
|
||||
const questionRepository: QuestionRepository = getQuestionRepository();
|
||||
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
||||
|
||||
if (!questions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (full) {
|
||||
return questions.map(mapToQuestionDTO);
|
||||
}
|
||||
|
@ -24,90 +19,57 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea
|
|||
return questions.map(mapToQuestionDTOId);
|
||||
}
|
||||
|
||||
async function fetchQuestion(questionId: QuestionId): Promise<Question | null> {
|
||||
export async function fetchQuestion(questionId: QuestionId): Promise<Question> {
|
||||
const questionRepository = getQuestionRepository();
|
||||
const question = await questionRepository.findByLearningObjectAndSequenceNumber(
|
||||
mapToLearningObjectID(questionId.learningObjectIdentifier),
|
||||
questionId.sequenceNumber
|
||||
);
|
||||
|
||||
return await questionRepository.findOne({
|
||||
learningObjectHruid: questionId.learningObjectIdentifier.hruid,
|
||||
learningObjectLanguage: questionId.learningObjectIdentifier.language,
|
||||
learningObjectVersion: questionId.learningObjectIdentifier.version,
|
||||
sequenceNumber: questionId.sequenceNumber,
|
||||
if (!question) {
|
||||
throw new NotFoundException('Question with loID and sequence number not found');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise<AnswerDTO[] | AnswerId[]> {
|
||||
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> {
|
||||
export async function deleteQuestion(questionId: QuestionId): Promise<QuestionDTO> {
|
||||
const questionRepository = getQuestionRepository();
|
||||
|
||||
const author = mapToStudent(questionDTO.author);
|
||||
const question = await fetchQuestion(questionId); // Throws error if not found
|
||||
|
||||
const loId: LearningObjectIdentifier = {
|
||||
...questionDTO.learningObjectIdentifier,
|
||||
version: questionDTO.learningObjectIdentifier.version ?? 1,
|
||||
hruid: questionId.learningObjectIdentifier.hruid,
|
||||
language: questionId.learningObjectIdentifier.language,
|
||||
version: questionId.learningObjectIdentifier.version || FALLBACK_VERSION_NUM,
|
||||
};
|
||||
|
||||
try {
|
||||
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;
|
||||
}
|
||||
|
||||
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(loId, questionId.sequenceNumber);
|
||||
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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue