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
|
@ -7,7 +7,7 @@
|
|||
"main": "dist/app.js",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production tsc --build",
|
||||
"dev": "cross-env NODE_ENV=development tsx watch --env-file=.env.development.local src/app.ts",
|
||||
"dev": "cross-env NODE_ENV=development tsx tool/seed.ts; tsx watch --env-file=.env.development.local src/app.ts",
|
||||
"start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js",
|
||||
"format": "prettier --write src/",
|
||||
"format-check": "prettier --check src/",
|
||||
|
|
|
@ -5,3 +5,4 @@ export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl);
|
|||
export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage);
|
||||
|
||||
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 { NotFoundException } from '../exceptions/not-found-exception.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) {
|
||||
throw new BadRequestException('HRUID is required.');
|
||||
}
|
||||
|
|
|
@ -1,34 +1,20 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js';
|
||||
import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js';
|
||||
import { createQuestion, deleteQuestion, getAllQuestions, getQuestion, updateQuestion } from '../services/questions.js';
|
||||
import { FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM } from '../config.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 { requireFields } from './error-helper.js';
|
||||
|
||||
function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null {
|
||||
const { hruid, version } = req.params;
|
||||
const lang = req.query.lang;
|
||||
|
||||
if (!hruid || !version) {
|
||||
res.status(400).json({ error: 'Missing required parameters.' });
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier {
|
||||
return {
|
||||
hruid,
|
||||
language: (lang as Language) || FALLBACK_LANG,
|
||||
version: Number(version),
|
||||
language: (lang || FALLBACK_LANG) as Language,
|
||||
version: Number(version) || FALLBACK_VERSION_NUM,
|
||||
};
|
||||
}
|
||||
|
||||
function getQuestionId(req: Request, res: Response): QuestionId | null {
|
||||
const seq = req.params.seq;
|
||||
const learningObjectIdentifier = getObjectId(req, res);
|
||||
|
||||
if (!learningObjectIdentifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getQuestionId(learningObjectIdentifier: LearningObjectIdentifier, seq: string): QuestionId {
|
||||
return {
|
||||
learningObjectIdentifier,
|
||||
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> {
|
||||
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';
|
||||
requireFields({ hruid });
|
||||
|
||||
if (!objectId) {
|
||||
return;
|
||||
}
|
||||
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||
|
||||
const questions = await getAllQuestions(objectId, full);
|
||||
const questions = await getAllQuestions(learningObjectId, full);
|
||||
|
||||
if (!questions) {
|
||||
res.status(404).json({ error: `Questions not found.` });
|
||||
} else {
|
||||
res.json({ questions: questions });
|
||||
}
|
||||
res.json({ questions });
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||
const questionId = getQuestionId(learningObjectId, seq);
|
||||
|
||||
const question = await getQuestion(questionId);
|
||||
|
||||
if (!question) {
|
||||
res.status(404).json({ error: `Question not found.` });
|
||||
} else {
|
||||
res.json(question);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getQuestionAnswersHandler(req: Request, res: Response): Promise<void> {
|
||||
const questionId = getQuestionId(req, res);
|
||||
const full = req.query.full === 'true';
|
||||
|
||||
if (!questionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const answers = await getAnswersByQuestion(questionId, full);
|
||||
|
||||
if (!answers) {
|
||||
res.status(404).json({ error: `Questions not found` });
|
||||
} else {
|
||||
res.json({ answers: answers });
|
||||
}
|
||||
res.json({ question });
|
||||
}
|
||||
|
||||
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) {
|
||||
res.status(400).json({ error: 'Missing required fields: identifier and content' });
|
||||
return;
|
||||
}
|
||||
const loId = getLearningObjectId(hruid, version, language);
|
||||
|
||||
const question = await createQuestion(questionDTO);
|
||||
const author = req.body.author as string;
|
||||
const content = req.body.content as string;
|
||||
requireFields({ author, content });
|
||||
|
||||
if (!question) {
|
||||
res.status(400).json({ error: 'Could not create question' });
|
||||
} else {
|
||||
res.json(question);
|
||||
}
|
||||
const questionData = req.body as QuestionData;
|
||||
|
||||
const question = await createQuestion(loId, questionData);
|
||||
|
||||
res.json({ question });
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||
const questionId = getQuestionId(learningObjectId, seq);
|
||||
|
||||
const question = await deleteQuestion(questionId);
|
||||
|
||||
if (!question) {
|
||||
res.status(400).json({ error: 'Could not find nor delete question' });
|
||||
} else {
|
||||
res.json(question);
|
||||
}
|
||||
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 { Question } from '../../entities/questions/question.entity.js';
|
||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||
import { Loaded } from '@mikro-orm/core';
|
||||
|
||||
export class AnswerRepository extends DwengoEntityRepository<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' },
|
||||
});
|
||||
}
|
||||
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> {
|
||||
return this.deleteWhere({
|
||||
toQuestion: question,
|
||||
sequenceNumber: sequenceNumber,
|
||||
});
|
||||
}
|
||||
public async updateContent(answer: Answer, newContent: string): Promise<Answer> {
|
||||
answer.content = newContent;
|
||||
await this.save(answer);
|
||||
return answer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object
|
|||
import { Student } from '../../entities/users/student.entity.js';
|
||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||
import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
||||
import { Loaded } from '@mikro-orm/core';
|
||||
|
||||
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||
public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
|
||||
|
@ -70,4 +71,19 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
|||
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 { Answer } from '../entities/questions/answer.entity.js';
|
||||
import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
|
||||
import { mapToTeacherDTO } from './teacher.js';
|
||||
|
||||
/**
|
||||
* Convert a Question entity to a DTO format.
|
||||
*/
|
||||
export function mapToAnswerDTO(answer: Answer): AnswerDTO {
|
||||
return {
|
||||
author: mapToUserDTO(answer.author),
|
||||
author: mapToTeacherDTO(answer.author),
|
||||
toQuestion: mapToQuestionDTO(answer.toQuestion),
|
||||
sequenceNumber: answer.sequenceNumber!,
|
||||
timestamp: answer.timestamp.toISOString(),
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Question } from '../entities/questions/question.entity.js';
|
||||
import { mapToStudentDTO } from './student.js';
|
||||
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 {
|
||||
hruid: question.learningObjectHruid,
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { EntityManager, MikroORM } from '@mikro-orm/core';
|
||||
import { EntityManager, IDatabaseDriver, MikroORM } from '@mikro-orm/core';
|
||||
import config from './mikro-orm.config.js';
|
||||
import { envVars, getEnvVar } from './util/envVars.js';
|
||||
import { getLogger, Logger } from './logging/initalize.js';
|
||||
|
||||
let orm: MikroORM | undefined;
|
||||
export async function initORM(testingMode = false): Promise<void> {
|
||||
export async function initORM(testingMode = false): Promise<MikroORM<IDatabaseDriver, EntityManager>> {
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
logger.info('Initializing ORM');
|
||||
|
@ -25,6 +25,8 @@ export async function initORM(testingMode = false): Promise<void> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
return orm;
|
||||
}
|
||||
export function forkEntityManager(): EntityManager {
|
||||
if (!orm) {
|
||||
|
|
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 {
|
||||
createQuestionHandler,
|
||||
deleteQuestionHandler,
|
||||
getAllQuestionsHandler,
|
||||
getQuestionAnswersHandler,
|
||||
getQuestionHandler,
|
||||
} from '../controllers/questions.js';
|
||||
import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js';
|
||||
import answerRoutes from './answers.js';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
// Query language
|
||||
|
@ -20,6 +16,6 @@ router.delete('/:seq', deleteQuestionHandler);
|
|||
// Information about a question with id
|
||||
router.get('/:seq', getQuestionHandler);
|
||||
|
||||
router.get('/answers/:seq', getQuestionAnswersHandler);
|
||||
router.use('/:seq/answers', answerRoutes);
|
||||
|
||||
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 { 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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
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}`;
|
||||
if (learningObjectId.version) {
|
||||
url += `&version=${learningObjectId.version}`;
|
||||
|
@ -17,7 +17,7 @@ export function getUrlStringForLearningObject(learningObjectId: LearningObjectId
|
|||
return url;
|
||||
}
|
||||
|
||||
export function getUrlStringForLearningObjectHTML(learningObjectIdentifier: LearningObjectIdentifier): string {
|
||||
export function getUrlStringForLearningObjectHTML(learningObjectIdentifier: LearningObjectIdentifierDTO): string {
|
||||
let url = `/learningObject/${learningObjectIdentifier.hruid}/html?language=${learningObjectIdentifier.language}`;
|
||||
if (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);
|
||||
});
|
||||
});
|
|
@ -186,7 +186,7 @@ describe('Student controllers', () => {
|
|||
|
||||
it('Get join request by student and class', async () => {
|
||||
req = {
|
||||
params: { username: 'PinkFloyd', classId: 'id02' },
|
||||
params: { username: 'PinkFloyd', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
||||
};
|
||||
|
||||
await getStudentRequestHandler(req as Request, res as Response);
|
||||
|
@ -201,7 +201,7 @@ describe('Student controllers', () => {
|
|||
it('Create and delete join request', async () => {
|
||||
req = {
|
||||
params: { username: 'TheDoors' },
|
||||
body: { classId: 'id02' },
|
||||
body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
||||
};
|
||||
|
||||
await createStudentRequestHandler(req as Request, res as Response);
|
||||
|
@ -209,7 +209,7 @@ describe('Student controllers', () => {
|
|||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() }));
|
||||
|
||||
req = {
|
||||
params: { username: 'TheDoors', classId: 'id02' },
|
||||
params: { username: 'TheDoors', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
||||
};
|
||||
|
||||
await deleteClassJoinRequestHandler(req as Request, res as Response);
|
||||
|
@ -231,7 +231,7 @@ describe('Student controllers', () => {
|
|||
it('Create join request duplicate', async () => {
|
||||
req = {
|
||||
params: { username: 'Tool' },
|
||||
body: { classId: 'id02' },
|
||||
body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
||||
};
|
||||
|
||||
await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException);
|
||||
|
|
|
@ -105,9 +105,9 @@ describe('Teacher controllers', () => {
|
|||
const result = jsonMock.mock.lastCall?.[0];
|
||||
|
||||
const teacherUsernames = result.teachers.map((s: TeacherDTO) => s.username);
|
||||
expect(teacherUsernames).toContain('FooFighters');
|
||||
expect(teacherUsernames).toContain('testleerkracht1');
|
||||
|
||||
expect(result.teachers).toHaveLength(4);
|
||||
expect(result.teachers).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('Deleting non-existent student', async () => {
|
||||
|
@ -118,7 +118,7 @@ describe('Teacher controllers', () => {
|
|||
|
||||
it('Get teacher classes', async () => {
|
||||
req = {
|
||||
params: { username: 'FooFighters' },
|
||||
params: { username: 'testleerkracht1' },
|
||||
query: { full: 'true' },
|
||||
};
|
||||
|
||||
|
@ -133,7 +133,7 @@ describe('Teacher controllers', () => {
|
|||
|
||||
it('Get teacher students', async () => {
|
||||
req = {
|
||||
params: { username: 'FooFighters' },
|
||||
params: { username: 'testleerkracht1' },
|
||||
query: { full: 'true' },
|
||||
};
|
||||
|
||||
|
@ -169,7 +169,7 @@ describe('Teacher controllers', () => {
|
|||
|
||||
it('Get join requests by class', async () => {
|
||||
req = {
|
||||
params: { classId: 'id02' },
|
||||
params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
||||
};
|
||||
|
||||
await getStudentJoinRequestHandler(req as Request, res as Response);
|
||||
|
@ -183,7 +183,7 @@ describe('Teacher controllers', () => {
|
|||
|
||||
it('Update join request status', async () => {
|
||||
req = {
|
||||
params: { classId: 'id02', studentUsername: 'PinkFloyd' },
|
||||
params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
||||
body: { accepted: 'true' },
|
||||
};
|
||||
|
||||
|
@ -201,7 +201,7 @@ describe('Teacher controllers', () => {
|
|||
expect(status).toBeTruthy();
|
||||
|
||||
req = {
|
||||
params: { id: 'id02' },
|
||||
params: { id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
||||
};
|
||||
|
||||
await getClassHandler(req as Request, res as Response);
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('AssignmentRepository', () => {
|
|||
});
|
||||
|
||||
it('should return the requested assignment', async () => {
|
||||
const class_ = await classRepository.findById('id02');
|
||||
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
|
||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 2);
|
||||
|
||||
expect(assignment).toBeTruthy();
|
||||
|
@ -23,7 +23,7 @@ describe('AssignmentRepository', () => {
|
|||
});
|
||||
|
||||
it('should return all assignments for a class', async () => {
|
||||
const class_ = await classRepository.findById('id02');
|
||||
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
|
||||
const assignments = await assignmentRepository.findAllAssignmentsInClass(class_!);
|
||||
|
||||
expect(assignments).toBeTruthy();
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('GroupRepository', () => {
|
|||
});
|
||||
|
||||
it('should return the requested group', async () => {
|
||||
const class_ = await classRepository.findById('id01');
|
||||
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 1);
|
||||
|
||||
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1);
|
||||
|
@ -27,7 +27,7 @@ describe('GroupRepository', () => {
|
|||
});
|
||||
|
||||
it('should return all groups for assignment', async () => {
|
||||
const class_ = await classRepository.findById('id01');
|
||||
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 1);
|
||||
|
||||
const groups = await groupRepository.findAllGroupsForAssignment(assignment!);
|
||||
|
@ -37,7 +37,7 @@ describe('GroupRepository', () => {
|
|||
});
|
||||
|
||||
it('should not find removed group', async () => {
|
||||
const class_ = await classRepository.findById('id02');
|
||||
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
|
||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 2);
|
||||
|
||||
await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, 1);
|
||||
|
|
|
@ -50,7 +50,7 @@ describe('SubmissionRepository', () => {
|
|||
|
||||
it('should find the most recent submission for a group', async () => {
|
||||
const id = new LearningObjectIdentifier('id03', Language.English, 1);
|
||||
const class_ = await classRepository.findById('id01');
|
||||
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 1);
|
||||
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1);
|
||||
const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, group!);
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('ClassJoinRequestRepository', () => {
|
|||
});
|
||||
|
||||
it('should list all requests to a single class', async () => {
|
||||
const class_ = await cassRepository.findById('id02');
|
||||
const class_ = await cassRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
|
||||
const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_!);
|
||||
|
||||
expect(requests).toBeTruthy();
|
||||
|
@ -35,7 +35,7 @@ describe('ClassJoinRequestRepository', () => {
|
|||
|
||||
it('should not find a removed request', async () => {
|
||||
const student = await studentRepository.findByUsername('SmashingPumpkins');
|
||||
const class_ = await cassRepository.findById('id03');
|
||||
const class_ = await cassRepository.findById('80dcc3e0-1811-4091-9361-42c0eee91cfa');
|
||||
await classJoinRequestRepository.deleteBy(student!, class_!);
|
||||
|
||||
const request = await classJoinRequestRepository.findAllRequestsBy(student!);
|
||||
|
|
|
@ -18,16 +18,16 @@ describe('ClassRepository', () => {
|
|||
});
|
||||
|
||||
it('should return requested class', async () => {
|
||||
const classVar = await classRepository.findById('id01');
|
||||
const classVar = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
||||
|
||||
expect(classVar).toBeTruthy();
|
||||
expect(classVar?.displayName).toBe('class01');
|
||||
});
|
||||
|
||||
it('class should be gone after deletion', async () => {
|
||||
await classRepository.deleteById('id04');
|
||||
await classRepository.deleteById('33d03536-83b8-4880-9982-9bbf2f908ddf');
|
||||
|
||||
const classVar = await classRepository.findById('id04');
|
||||
const classVar = await classRepository.findById('33d03536-83b8-4880-9982-9bbf2f908ddf');
|
||||
|
||||
expect(classVar).toBeNull();
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('ClassRepository', () => {
|
|||
});
|
||||
|
||||
it('should return all invitations for a class', async () => {
|
||||
const class_ = await classRepository.findById('id02');
|
||||
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
|
||||
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_!);
|
||||
|
||||
expect(invitations).toBeTruthy();
|
||||
|
@ -42,7 +42,7 @@ describe('ClassRepository', () => {
|
|||
});
|
||||
|
||||
it('should not find a removed invitation', async () => {
|
||||
const class_ = await classRepository.findById('id01');
|
||||
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
||||
const sender = await teacherRepository.findByUsername('FooFighters');
|
||||
const receiver = await teacherRepository.findByUsername('LimpBizkit');
|
||||
await teacherInvitationRepository.deleteBy(class_!, sender!, receiver!);
|
||||
|
|
|
@ -7,11 +7,11 @@ import learningObjectService from '../../../src/services/learning-objects/learni
|
|||
import { envVars, getEnvVar } from '../../../src/util/envVars';
|
||||
import { LearningPath } from '../../../src/entities/content/learning-path.entity';
|
||||
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';
|
||||
|
||||
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',
|
||||
language: Language.Dutch,
|
||||
version: 3,
|
||||
|
|
|
@ -4,11 +4,11 @@ import { Student } from '../../../src/entities/users/student.entity';
|
|||
import { Teacher } from '../../../src/entities/users/teacher.entity';
|
||||
|
||||
export function makeTestClasses(em: EntityManager, students: Student[], teachers: Teacher[]): Class[] {
|
||||
const studentsClass01 = students.slice(0, 7);
|
||||
const teacherClass01: Teacher[] = teachers.slice(0, 1);
|
||||
const studentsClass01 = students.slice(0, 8);
|
||||
const teacherClass01: Teacher[] = teachers.slice(4, 5);
|
||||
|
||||
const class01 = em.create(Class, {
|
||||
classId: 'id01',
|
||||
classId: '8764b861-90a6-42e5-9732-c0d9eb2f55f9',
|
||||
displayName: 'class01',
|
||||
teachers: teacherClass01,
|
||||
students: studentsClass01,
|
||||
|
@ -18,7 +18,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
|
|||
const teacherClass02: Teacher[] = teachers.slice(1, 2);
|
||||
|
||||
const class02 = em.create(Class, {
|
||||
classId: 'id02',
|
||||
classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89',
|
||||
displayName: 'class02',
|
||||
teachers: teacherClass02,
|
||||
students: studentsClass02,
|
||||
|
@ -28,7 +28,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
|
|||
const teacherClass03: Teacher[] = teachers.slice(2, 3);
|
||||
|
||||
const class03 = em.create(Class, {
|
||||
classId: 'id03',
|
||||
classId: '80dcc3e0-1811-4091-9361-42c0eee91cfa',
|
||||
displayName: 'class03',
|
||||
teachers: teacherClass03,
|
||||
students: studentsClass03,
|
||||
|
@ -38,7 +38,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
|
|||
const teacherClass04: Teacher[] = teachers.slice(2, 3);
|
||||
|
||||
const class04 = em.create(Class, {
|
||||
classId: 'id04',
|
||||
classId: '33d03536-83b8-4880-9982-9bbf2f908ddf',
|
||||
displayName: 'class04',
|
||||
teachers: teacherClass04,
|
||||
students: studentsClass04,
|
||||
|
|
|
@ -11,6 +11,8 @@ export const TEST_STUDENTS = [
|
|||
{ username: 'TheDoors', firstName: 'Jim', lastName: 'Morisson' },
|
||||
// ⚠️ Deze mag niet gebruikt worden in elke test!
|
||||
{ username: 'Nirvana', firstName: 'Kurt', lastName: 'Cobain' },
|
||||
// Makes sure when logged in as leerling1, there exists a corresponding user
|
||||
{ username: 'testleerling1', firstName: 'Gerald', lastName: 'Schmittinger' },
|
||||
];
|
||||
|
||||
// 🏗️ Functie die ORM entities maakt uit de data array
|
||||
|
|
|
@ -27,5 +27,12 @@ export function makeTestTeachers(em: EntityManager): Teacher[] {
|
|||
lastName: 'Cappelle',
|
||||
});
|
||||
|
||||
return [teacher01, teacher02, teacher03, teacher04];
|
||||
// Makes sure when logged in as testleerkracht1, there exists a corresponding user
|
||||
const teacher05 = em.create(Teacher, {
|
||||
username: 'testleerkracht1',
|
||||
firstName: 'Bob',
|
||||
lastName: 'Dylan',
|
||||
});
|
||||
|
||||
return [teacher01, teacher02, teacher03, teacher04, teacher05];
|
||||
}
|
||||
|
|
72
backend/tool/seed.ts
Normal file
72
backend/tool/seed.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { forkEntityManager, initORM } from '../src/orm.js';
|
||||
import dotenv from 'dotenv';
|
||||
import { makeTestAssignemnts } from '../tests/test_assets/assignments/assignments.testdata.js';
|
||||
import { makeTestGroups } from '../tests/test_assets/assignments/groups.testdata.js';
|
||||
import { makeTestSubmissions } from '../tests/test_assets/assignments/submission.testdata.js';
|
||||
import { makeTestClassJoinRequests } from '../tests/test_assets/classes/class-join-requests.testdata.js';
|
||||
import { makeTestClasses } from '../tests/test_assets/classes/classes.testdata.js';
|
||||
import { makeTestTeacherInvitations } from '../tests/test_assets/classes/teacher-invitations.testdata.js';
|
||||
import { makeTestAttachments } from '../tests/test_assets/content/attachments.testdata.js';
|
||||
import { makeTestLearningObjects } from '../tests/test_assets/content/learning-objects.testdata.js';
|
||||
import { makeTestLearningPaths } from '../tests/test_assets/content/learning-paths.testdata.js';
|
||||
import { makeTestAnswers } from '../tests/test_assets/questions/answers.testdata.js';
|
||||
import { makeTestQuestions } from '../tests/test_assets/questions/questions.testdata.js';
|
||||
import { makeTestStudents } from '../tests/test_assets/users/students.testdata.js';
|
||||
import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js';
|
||||
import { getLogger, Logger } from '../src/logging/initalize.js';
|
||||
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
export async function seedDatabase(): Promise<void> {
|
||||
dotenv.config({ path: '.env.development.local' });
|
||||
const orm = await initORM();
|
||||
await orm.schema.clearDatabase();
|
||||
|
||||
const em = forkEntityManager();
|
||||
|
||||
logger.info('seeding database...');
|
||||
|
||||
const students = makeTestStudents(em);
|
||||
const teachers = makeTestTeachers(em);
|
||||
const learningObjects = makeTestLearningObjects(em);
|
||||
const learningPaths = makeTestLearningPaths(em);
|
||||
const classes = makeTestClasses(em, students, teachers);
|
||||
const assignments = makeTestAssignemnts(em, classes);
|
||||
const groups = makeTestGroups(em, students, assignments);
|
||||
|
||||
assignments[0].groups = groups.slice(0, 3);
|
||||
assignments[1].groups = groups.slice(3, 4);
|
||||
|
||||
const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes);
|
||||
const classJoinRequests = makeTestClassJoinRequests(em, students, classes);
|
||||
const attachments = makeTestAttachments(em, learningObjects);
|
||||
|
||||
learningObjects[1].attachments = attachments;
|
||||
|
||||
const questions = makeTestQuestions(em, students);
|
||||
const answers = makeTestAnswers(em, teachers, questions);
|
||||
const submissions = makeTestSubmissions(em, students, groups);
|
||||
|
||||
// Persist all entities
|
||||
await em.persistAndFlush([
|
||||
...students,
|
||||
...teachers,
|
||||
...learningObjects,
|
||||
...learningPaths,
|
||||
...classes,
|
||||
...assignments,
|
||||
...groups,
|
||||
...teacherInvitations,
|
||||
...classJoinRequests,
|
||||
...attachments,
|
||||
...questions,
|
||||
...answers,
|
||||
...submissions,
|
||||
]);
|
||||
|
||||
logger.info('Development database seeded successfully!');
|
||||
|
||||
await orm.close();
|
||||
}
|
||||
|
||||
seedDatabase().catch(logger.error);
|
|
@ -1,14 +1,19 @@
|
|||
import { UserDTO } from './user';
|
||||
import { QuestionDTO, QuestionId } from './question';
|
||||
import { TeacherDTO } from './teacher';
|
||||
|
||||
export interface AnswerDTO {
|
||||
author: UserDTO;
|
||||
author: TeacherDTO;
|
||||
toQuestion: QuestionDTO;
|
||||
sequenceNumber: number;
|
||||
timestamp: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AnswerData {
|
||||
author: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AnswerId {
|
||||
author: string;
|
||||
toQuestion: QuestionId;
|
||||
|
|
|
@ -11,7 +11,7 @@ export interface Transition {
|
|||
};
|
||||
}
|
||||
|
||||
export interface LearningObjectIdentifier {
|
||||
export interface LearningObjectIdentifierDTO {
|
||||
hruid: string;
|
||||
language: Language;
|
||||
version?: number;
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import { LearningObjectIdentifier } from './learning-content';
|
||||
import { LearningObjectIdentifierDTO } from './learning-content';
|
||||
import { StudentDTO } from './student';
|
||||
|
||||
export interface QuestionDTO {
|
||||
learningObjectIdentifier: LearningObjectIdentifier;
|
||||
learningObjectIdentifier: LearningObjectIdentifierDTO;
|
||||
sequenceNumber?: number;
|
||||
author: StudentDTO;
|
||||
timestamp?: string;
|
||||
timestamp: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface QuestionData {
|
||||
author?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface QuestionId {
|
||||
learningObjectIdentifier: LearningObjectIdentifier;
|
||||
learningObjectIdentifier: LearningObjectIdentifierDTO;
|
||||
sequenceNumber: number;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { GroupDTO } from './group';
|
||||
import { LearningObjectIdentifier } from './learning-content';
|
||||
import { LearningObjectIdentifierDTO } from './learning-content';
|
||||
import { StudentDTO } from './student';
|
||||
import { Language } from '../util/language';
|
||||
|
||||
export interface SubmissionDTO {
|
||||
learningObjectIdentifier: LearningObjectIdentifier;
|
||||
learningObjectIdentifier: LearningObjectIdentifierDTO;
|
||||
|
||||
submissionNumber?: number;
|
||||
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 });
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ export abstract class BaseController {
|
|||
}
|
||||
|
||||
private static assertSuccessResponse(response: AxiosResponse<unknown, unknown>): void {
|
||||
if (response.status / 100 !== 2) {
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new HttpErrorResponseException(response);
|
||||
}
|
||||
}
|
||||
|
@ -21,20 +21,20 @@ export abstract class BaseController {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
protected async post<T>(path: string, body: unknown): Promise<T> {
|
||||
const response = await apiClient.post<T>(this.absolutePathFor(path), body);
|
||||
protected async post<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> {
|
||||
const response = await apiClient.post<T>(this.absolutePathFor(path), body, { params: queryParams });
|
||||
BaseController.assertSuccessResponse(response);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async delete<T>(path: string): Promise<T> {
|
||||
const response = await apiClient.delete<T>(this.absolutePathFor(path));
|
||||
protected async delete<T>(path: string, queryParams?: QueryParams): Promise<T> {
|
||||
const response = await apiClient.delete<T>(this.absolutePathFor(path), { params: queryParams });
|
||||
BaseController.assertSuccessResponse(response);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async put<T>(path: string, body: unknown): Promise<T> {
|
||||
const response = await apiClient.put<T>(this.absolutePathFor(path), body);
|
||||
protected async put<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> {
|
||||
const response = await apiClient.put<T>(this.absolutePathFor(path), body, { params: queryParams });
|
||||
BaseController.assertSuccessResponse(response);
|
||||
return response.data;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ThemeController } from "@/controllers/themes.ts";
|
||||
import { LearningObjectController } from "@/controllers/learning-objects.ts";
|
||||
import { LearningPathController } from "@/controllers/learning-paths.ts";
|
||||
import { ClassController } from "@/controllers/classes.ts";
|
||||
|
||||
export function controllerGetter<T>(factory: new () => T): () => T {
|
||||
let instance: T | undefined;
|
||||
|
@ -16,3 +17,4 @@ export function controllerGetter<T>(factory: new () => T): () => T {
|
|||
export const getThemeController = controllerGetter(ThemeController);
|
||||
export const getLearningObjectController = controllerGetter(LearningObjectController);
|
||||
export const getLearningPathController = controllerGetter(LearningPathController);
|
||||
export const getClassController = controllerGetter(ClassController);
|
||||
|
|
|
@ -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 {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ export class StudentController extends BaseController {
|
|||
}
|
||||
|
||||
async createJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> {
|
||||
return this.post<JoinRequestResponse>(`/${username}/joinRequests}`, classId);
|
||||
return this.post<JoinRequestResponse>(`/${username}/joinRequests`, { classId });
|
||||
}
|
||||
|
||||
async deleteJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> {
|
||||
|
|
|
@ -17,6 +17,11 @@
|
|||
"inclusive": "Inclusiv",
|
||||
"sociallyRelevant": "Gesellschaftlich relevant",
|
||||
"translate": "übersetzen",
|
||||
"joinClass": "Klasse beitreten",
|
||||
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
|
||||
"invalidFormat": "Ungültiges Format",
|
||||
"submitCode": "senden",
|
||||
"members": "Mitglieder",
|
||||
"themes": "Themen",
|
||||
"choose-theme": "Wähle ein thema",
|
||||
"choose-age": "Alter auswählen",
|
||||
|
@ -51,5 +56,24 @@
|
|||
"noLearningPathsFoundDescription": "Es gibt keine Lernpfade, die zu Ihrem Suchbegriff passen.",
|
||||
"legendNotCompletedYet": "Noch nicht fertig",
|
||||
"legendCompleted": "Fertig",
|
||||
"legendTeacherExclusive": "Information für Lehrkräfte"
|
||||
"legendTeacherExclusive": "Information für Lehrkräfte",
|
||||
"code": "code",
|
||||
"class": "Klasse",
|
||||
"invitations": "Einladungen",
|
||||
"createClass": "Klasse erstellen",
|
||||
"createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.",
|
||||
"classname": "Klassenname",
|
||||
"EnterNameOfClass": "einen Klassennamen eingeben.",
|
||||
"create": "erstellen",
|
||||
"sender": "Absender",
|
||||
"nameIsMandatory": "Der Klassenname ist ein Pflichtfeld",
|
||||
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
|
||||
"close": "schließen",
|
||||
"copied": "kopiert!",
|
||||
"accept": "akzeptieren",
|
||||
"deny": "ablehnen",
|
||||
"sent": "sent",
|
||||
"failed": "gescheitert",
|
||||
"wrong": "etwas ist schief gelaufen",
|
||||
"created": "erstellt"
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
"sociallyRelevant": "Socially relevant",
|
||||
"login": "log in",
|
||||
"translate": "translate",
|
||||
"joinClass": "Join class",
|
||||
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
|
||||
"invalidFormat": "Invalid format.",
|
||||
"submitCode": "submit",
|
||||
"members": "members",
|
||||
"themes": "Themes",
|
||||
"choose-theme": "Select a theme",
|
||||
"choose-age": "Select age",
|
||||
|
@ -51,5 +56,24 @@
|
|||
"high-school": "16-18 years old",
|
||||
"older": "18 and older"
|
||||
},
|
||||
"read-more": "Read more"
|
||||
"read-more": "Read more",
|
||||
"code": "code",
|
||||
"class": "class",
|
||||
"invitations": "invitations",
|
||||
"createClass": "create class",
|
||||
"classname": "classname",
|
||||
"EnterNameOfClass": "Enter a classname.",
|
||||
"create": "create",
|
||||
"sender": "sender",
|
||||
"nameIsMandatory": "classname is mandatory",
|
||||
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
|
||||
"close": "close",
|
||||
"copied": "copied!",
|
||||
"accept": "accept",
|
||||
"deny": "deny",
|
||||
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.",
|
||||
"sent": "sent",
|
||||
"failed": "failed",
|
||||
"wrong": "something went wrong",
|
||||
"created": "created"
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
"inclusive": "Inclusif",
|
||||
"sociallyRelevant": "Socialement pertinent",
|
||||
"translate": "traduire",
|
||||
"joinClass": "Rejoindre une classe",
|
||||
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
|
||||
"invalidFormat": "Format non valide.",
|
||||
"submitCode": "envoyer",
|
||||
"members": "membres",
|
||||
"themes": "Thèmes",
|
||||
"choose-theme": "Choisis un thème",
|
||||
"choose-age": "Choisis un âge",
|
||||
|
@ -51,5 +56,24 @@
|
|||
"high-school": "16-18 ans",
|
||||
"older": "18 et plus"
|
||||
},
|
||||
"read-more": "En savoir plus"
|
||||
"read-more": "En savoir plus",
|
||||
"code": "code",
|
||||
"class": "classe",
|
||||
"invitations": "invitations",
|
||||
"createClass": "créer une classe",
|
||||
"createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.",
|
||||
"classname": "nom de classe",
|
||||
"EnterNameOfClass": "saisir un nom de classe.",
|
||||
"create": "créer",
|
||||
"sender": "expéditeur",
|
||||
"nameIsMandatory": "le nom de classe est obligatoire",
|
||||
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
|
||||
"close": "fermer",
|
||||
"copied": "copié!",
|
||||
"accept": "accepter",
|
||||
"deny": "refuser",
|
||||
"sent": "envoyé",
|
||||
"failed": "échoué",
|
||||
"wrong": "quelque chose n'a pas fonctionné",
|
||||
"created": "créé"
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
"sociallyRelevant": "Maatschappelijk relevant",
|
||||
"login": "log in",
|
||||
"translate": "vertalen",
|
||||
"joinClass": "Word lid van een klas",
|
||||
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
|
||||
"invalidFormat": "Ongeldig formaat.",
|
||||
"submitCode": "verzenden",
|
||||
"members": "leden",
|
||||
"themes": "Lesthema's",
|
||||
"choose-theme": "Kies een thema",
|
||||
"choose-age": "Kies een leeftijd",
|
||||
|
@ -51,5 +56,24 @@
|
|||
"high-school": "3e graad secundair",
|
||||
"older": "Hoger onderwijs"
|
||||
},
|
||||
"read-more": "Lees meer"
|
||||
"read-more": "Lees meer",
|
||||
"code": "code",
|
||||
"class": "klas",
|
||||
"invitations": "uitnodigingen",
|
||||
"createClass": "klas aanmaken",
|
||||
"createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.",
|
||||
"classname": "klasnaam",
|
||||
"EnterNameOfClass": "Geef een klasnaam op.",
|
||||
"create": "aanmaken",
|
||||
"sender": "afzender",
|
||||
"nameIsMandatory": "klasnaam is verplicht",
|
||||
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
|
||||
"close": "sluiten",
|
||||
"copied": "gekopieerd!",
|
||||
"accept": "accepteren",
|
||||
"deny": "weigeren",
|
||||
"sent": "verzonden",
|
||||
"failed": "mislukt",
|
||||
"wrong": "er ging iets verkeerd",
|
||||
"created": "gecreëerd"
|
||||
}
|
||||
|
|
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)) });
|
||||
},
|
||||
});
|
||||
}
|
|
@ -19,7 +19,7 @@ import type { AssignmentsResponse } from "@/controllers/assignments.ts";
|
|||
import type { GroupsResponse } from "@/controllers/groups.ts";
|
||||
import type { SubmissionsResponse } from "@/controllers/submissions.ts";
|
||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
||||
import type { StudentDTO } from "@dwengo-1/interfaces/student";
|
||||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||
|
||||
const studentController = new StudentController();
|
||||
|
||||
|
@ -179,7 +179,7 @@ export function useCreateJoinRequestMutation(): UseMutationReturnType<
|
|||
mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId),
|
||||
onSuccess: async (newJoinRequest) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester),
|
||||
queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester.username),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -196,7 +196,7 @@ export function useDeleteJoinRequestMutation(): UseMutationReturnType<
|
|||
return useMutation({
|
||||
mutationFn: async ({ username, classId }) => studentController.deleteJoinRequest(username, classId),
|
||||
onSuccess: async (deletedJoinRequest) => {
|
||||
const username = deletedJoinRequest.request.requester;
|
||||
const username = deletedJoinRequest.request.requester.username;
|
||||
const classId = deletedJoinRequest.request.class;
|
||||
await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) });
|
||||
await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) });
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import { computed, toValue } from "vue";
|
||||
import type { MaybeRefOrGetter } from "vue";
|
||||
import { useMutation, useQuery, useQueryClient, UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query";
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseMutationReturnType,
|
||||
type UseQueryReturnType,
|
||||
} from "@tanstack/vue-query";
|
||||
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
|
||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
||||
import type { TeacherDTO } from "@dwengo-1/interfaces/teacher";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
|
||||
|
||||
const teacherController = new TeacherController();
|
||||
|
|
|
@ -1,7 +1,224 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import authState from "@/services/auth/auth-service.ts";
|
||||
import { onMounted, ref } from "vue";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ClassController, type ClassResponse } from "@/controllers/classes";
|
||||
import type { StudentsResponse } from "@/controllers/students";
|
||||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Username of logged in teacher
|
||||
const username = ref<string | undefined>(undefined);
|
||||
const classController: ClassController = new ClassController();
|
||||
|
||||
// Find class id from route
|
||||
const route = useRoute();
|
||||
const classId: string = route.params.id as string;
|
||||
|
||||
const isLoading = ref(true);
|
||||
const currentClass = ref<ClassDTO | undefined>(undefined);
|
||||
const students = ref<StudentDTO[]>([]);
|
||||
|
||||
// Find the username of the logged in user so it can be used to fetch other information
|
||||
// When loading the page
|
||||
onMounted(async () => {
|
||||
const userObject = await authState.loadUser();
|
||||
username.value = userObject?.profile?.preferred_username ?? undefined;
|
||||
|
||||
// Get class of which information should be shown
|
||||
const classResponse: ClassResponse = await classController.getById(classId);
|
||||
if (classResponse && classResponse.class) {
|
||||
currentClass.value = classResponse.class;
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
// Fetch all students of the class
|
||||
const studentsResponse: StudentsResponse = await classController.getStudents(classId);
|
||||
if (studentsResponse && studentsResponse.students) students.value = studentsResponse.students as StudentDTO[];
|
||||
});
|
||||
|
||||
// TODO: Boolean that handles visibility for dialogs
|
||||
// Popup to verify removing student
|
||||
const dialog = ref(false);
|
||||
const selectedStudent = ref<StudentDTO | null>(null);
|
||||
|
||||
function showPopup(s: StudentDTO): void {
|
||||
selectedStudent.value = s;
|
||||
dialog.value = true;
|
||||
}
|
||||
|
||||
// Remove student from class
|
||||
function removeStudentFromclass(): void {
|
||||
dialog.value = false;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
<main>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="text-center py-10"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ currentClass!.displayName }}</h1>
|
||||
<v-container
|
||||
fluid
|
||||
class="ma-4"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
fluid
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="6"
|
||||
>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("students") }}</th>
|
||||
<th class="header"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="s in students"
|
||||
:key="s.id"
|
||||
>
|
||||
<td>
|
||||
{{ s.firstName + " " + s.lastName }}
|
||||
</td>
|
||||
<td>
|
||||
<v-btn @click="showPopup"> {{ t("remove") }} </v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">{{ t("areusure") }}</v-card-title>
|
||||
|
||||
<style scoped></style>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
text
|
||||
@click="dialog = false"
|
||||
>
|
||||
{{ t("cancel") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
@click="removeStudentFromclass"
|
||||
>{{ t("yes") }}</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</main>
|
||||
</template>
|
||||
<style scoped>
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.table thead th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even) {
|
||||
background-color: #f6faf2;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
padding-top: 2%;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0b75bb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,380 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import authState from "@/services/auth/auth-service.ts";
|
||||
import { computed, onMounted, ref, type ComputedRef } from "vue";
|
||||
import { validate, version } from "uuid";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students";
|
||||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||
import { StudentController } from "@/controllers/students";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
import { TeacherController } from "@/controllers/teachers";
|
||||
|
||||
const { t } = useI18n();
|
||||
const studentController: StudentController = new StudentController();
|
||||
const teacherController: TeacherController = new TeacherController();
|
||||
|
||||
// Username of logged in student
|
||||
const username = ref<string | undefined>(undefined);
|
||||
|
||||
// Find the username of the logged in user so it can be used to fetch other information
|
||||
// When loading the page
|
||||
onMounted(async () => {
|
||||
const userObject = await authState.loadUser();
|
||||
username.value = userObject?.profile?.preferred_username ?? undefined;
|
||||
});
|
||||
|
||||
// Fetch all classes of the logged in student
|
||||
const { data: classesResponse, isLoading, error } = useStudentClassesQuery(username);
|
||||
|
||||
// Empty list when classes are not yet loaded, else the list of classes of the user
|
||||
const classes: ComputedRef<ClassDTO[]> = computed(() => {
|
||||
// The classes are not yet fetched
|
||||
if (!classesResponse.value) {
|
||||
return [];
|
||||
}
|
||||
// The user has no classes
|
||||
if (classesResponse.value.classes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return classesResponse.value.classes as ClassDTO[];
|
||||
});
|
||||
|
||||
// Students of selected class are shown when logged in student presses on the member count
|
||||
const selectedClass = ref<ClassDTO | null>(null);
|
||||
const students = ref<StudentDTO[]>([]);
|
||||
const teachers = ref<TeacherDTO[]>([]);
|
||||
const getStudents = ref(false);
|
||||
|
||||
// Boolean that handles visibility for dialogs
|
||||
// Clicking on membercount will show a dialog with all members
|
||||
const dialog = ref(false);
|
||||
|
||||
// Function to display all members of a class in a dialog
|
||||
async function openStudentDialog(c: ClassDTO): Promise<void> {
|
||||
selectedClass.value = c;
|
||||
|
||||
// Clear previous value
|
||||
getStudents.value = true;
|
||||
students.value = [];
|
||||
dialog.value = true;
|
||||
|
||||
// Fetch students from their usernames to display their full names
|
||||
const studentDTOs: (StudentDTO | null)[] = await Promise.all(
|
||||
c.students.map(async (uid) => {
|
||||
try {
|
||||
const res = await studentController.getByUsername(uid);
|
||||
return res.student;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Only show students that are not fetched ass *null*
|
||||
students.value = studentDTOs.filter(Boolean) as StudentDTO[];
|
||||
}
|
||||
|
||||
async function openTeacherDialog(c: ClassDTO): Promise<void> {
|
||||
selectedClass.value = c;
|
||||
|
||||
// Clear previous value
|
||||
getStudents.value = false;
|
||||
teachers.value = [];
|
||||
dialog.value = true;
|
||||
|
||||
// Fetch names of teachers
|
||||
const teacherDTOs: (TeacherDTO | null)[] = await Promise.all(
|
||||
c.teachers.map(async (uid) => {
|
||||
try {
|
||||
const res = await teacherController.getByUsername(uid);
|
||||
return res.teacher;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
teachers.value = teacherDTOs.filter(Boolean) as TeacherDTO[];
|
||||
}
|
||||
|
||||
// Hold the code a student gives in to join a class
|
||||
const code = ref<string>("");
|
||||
|
||||
// The code a student sends in to join a class needs to be formatted as v4 to be valid
|
||||
// These rules are used to display a message to the user if they use a code that has an invalid format
|
||||
const codeRules = [
|
||||
(value: string | undefined): string | boolean => {
|
||||
if (value === undefined || value === "") {
|
||||
return true;
|
||||
} else if (value !== undefined && validate(value) && version(value) === 4) {
|
||||
return true;
|
||||
}
|
||||
return t("invalidFormat");
|
||||
},
|
||||
];
|
||||
|
||||
// Used to send the actual class join request
|
||||
const { mutate } = useCreateJoinRequestMutation();
|
||||
|
||||
// Function called when a student submits a code to join a class
|
||||
function submitCode(): void {
|
||||
// Check if the code is valid
|
||||
if (code.value !== undefined && validate(code.value) && version(code.value) === 4) {
|
||||
mutate(
|
||||
{ username: username.value!, classId: code.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showSnackbar(t("sent"), "success");
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
},
|
||||
},
|
||||
);
|
||||
code.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
const snackbar = ref({
|
||||
visible: false,
|
||||
message: "",
|
||||
color: "success",
|
||||
});
|
||||
|
||||
function showSnackbar(message: string, color: string): void {
|
||||
snackbar.value.message = message;
|
||||
snackbar.value.color = color;
|
||||
snackbar.value.visible = true;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
<main>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="text-center py-10"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
<style scoped></style>
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="text-center py-10 text-error"
|
||||
>
|
||||
<v-icon large>mdi-alert-circle</v-icon>
|
||||
<p>Error loading: {{ error.message }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ t("classes") }}</h1>
|
||||
<v-container
|
||||
fluid
|
||||
class="ma-4"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
fluid
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="6"
|
||||
>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("classes") }}</th>
|
||||
<th class="header">{{ t("teachers") }}</th>
|
||||
<th class="header">{{ t("members") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="c in classes"
|
||||
:key="c.id"
|
||||
>
|
||||
<td>{{ c.displayName }}</td>
|
||||
<td
|
||||
class="link"
|
||||
@click="openTeacherDialog(c)"
|
||||
>
|
||||
{{ c.teachers.length }}
|
||||
</td>
|
||||
<td
|
||||
class="link"
|
||||
@click="openStudentDialog(c)"
|
||||
>
|
||||
{{ c.students.length }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title> {{ selectedClass?.displayName }} </v-card-title>
|
||||
<v-card-text>
|
||||
<ul v-if="getStudents">
|
||||
<li
|
||||
v-for="student in students"
|
||||
:key="student.username"
|
||||
>
|
||||
{{ student.firstName + " " + student.lastName }}
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-else>
|
||||
<li
|
||||
v-for="teacher in teachers"
|
||||
:key="teacher.username"
|
||||
>
|
||||
{{ teacher.firstName + " " + teacher.lastName }}
|
||||
</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="dialog = false"
|
||||
>Close</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<div>
|
||||
<div class="join">
|
||||
<h2>{{ t("joinClass") }}</h2>
|
||||
<p>{{ t("JoinClassExplanation") }}</p>
|
||||
|
||||
<v-sheet
|
||||
class="pa-4 sheet"
|
||||
max-width="400"
|
||||
>
|
||||
<v-form @submit.prevent>
|
||||
<v-text-field
|
||||
label="CODE"
|
||||
v-model="code"
|
||||
placeholder="XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX"
|
||||
:rules="codeRules"
|
||||
variant="outlined"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
class="mt-4"
|
||||
color="#f6faf2"
|
||||
type="submit"
|
||||
@click="submitCode"
|
||||
block
|
||||
>{{ t("submitCode") }}</v-btn
|
||||
>
|
||||
</v-form>
|
||||
</v-sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-snackbar
|
||||
v-model="snackbar.visible"
|
||||
:color="snackbar.color"
|
||||
timeout="3000"
|
||||
>
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</main>
|
||||
</template>
|
||||
<style scoped>
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.table thead th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even) {
|
||||
background-color: #f6faf2;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
padding-top: 2%;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0b75bb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,404 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import authState from "@/services/auth/auth-service.ts";
|
||||
import { computed, onMounted, ref, type ComputedRef } from "vue";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
|
||||
import { useTeacherClassesQuery } from "@/queries/teachers";
|
||||
import { ClassController, type ClassResponse } from "@/controllers/classes";
|
||||
|
||||
const { t } = useI18n();
|
||||
const classController = new ClassController();
|
||||
|
||||
// Username of logged in teacher
|
||||
const username = ref<string | undefined>(undefined);
|
||||
|
||||
// Find the username of the logged in user so it can be used to fetch other information
|
||||
// When loading the page
|
||||
onMounted(async () => {
|
||||
const userObject = await authState.loadUser();
|
||||
username.value = userObject?.profile?.preferred_username ?? undefined;
|
||||
});
|
||||
|
||||
// Fetch all classes of the logged in teacher
|
||||
const { data: classesResponse, isLoading, error, refetch } = useTeacherClassesQuery(username, true);
|
||||
|
||||
// Empty list when classes are not yet loaded, else the list of classes of the user
|
||||
const classes: ComputedRef<ClassDTO[]> = computed(() => {
|
||||
// The classes are not yet fetched
|
||||
if (!classesResponse.value) {
|
||||
return [];
|
||||
}
|
||||
// The user has no classes
|
||||
if (classesResponse.value.classes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return classesResponse.value.classes as ClassDTO[];
|
||||
});
|
||||
|
||||
// Boolean that handles visibility for dialogs
|
||||
// Creating a class will generate a popup with the generated code
|
||||
const dialog = ref(false);
|
||||
|
||||
// Code generated when new class was created
|
||||
const code = ref<string>("");
|
||||
|
||||
// TODO: waiting on frontend controllers
|
||||
const invitations = ref<TeacherInvitationDTO[]>([]);
|
||||
|
||||
// Function to handle a accepted invitation request
|
||||
function acceptRequest(): void {
|
||||
//TODO: avoid linting issues when merging by filling the function
|
||||
invitations.value = [];
|
||||
}
|
||||
|
||||
// Function to handle a denied invitation request
|
||||
function denyRequest(): void {
|
||||
//TODO: avoid linting issues when merging by filling the function
|
||||
invitations.value = [];
|
||||
}
|
||||
|
||||
// Teacher should be able to set a displayname when making a class
|
||||
const className = ref<string>("");
|
||||
|
||||
// The name can only contain dash, underscore letters and numbers
|
||||
// These rules are used to display a message to the user if the name is not valid
|
||||
const nameRules = [
|
||||
(value: string | undefined): string | boolean => {
|
||||
if (!value) return true;
|
||||
if (value && /^[a-zA-Z0-9_-]+$/.test(value)) return true;
|
||||
return t("onlyUse");
|
||||
},
|
||||
];
|
||||
|
||||
// Function called when a teacher creates a class
|
||||
async function createClass(): Promise<void> {
|
||||
// Check if the class name is valid
|
||||
if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) {
|
||||
try {
|
||||
const classDto: ClassDTO = {
|
||||
id: "",
|
||||
displayName: className.value,
|
||||
teachers: [username.value!],
|
||||
students: [],
|
||||
joinRequests: [],
|
||||
};
|
||||
const classResponse: ClassResponse = await classController.createClass(classDto);
|
||||
const createdClass: ClassDTO = classResponse.class;
|
||||
code.value = createdClass.id;
|
||||
dialog.value = true;
|
||||
showSnackbar(t("created"), "success");
|
||||
|
||||
// Reload the table with classes so the new class appears
|
||||
await refetch();
|
||||
} catch (_) {
|
||||
showSnackbar(t("wrong"), "error");
|
||||
}
|
||||
}
|
||||
if (!className.value || className.value === "") {
|
||||
showSnackbar(t("name is mandatory"), "error");
|
||||
}
|
||||
}
|
||||
|
||||
const snackbar = ref({
|
||||
visible: false,
|
||||
message: "",
|
||||
color: "success",
|
||||
});
|
||||
|
||||
function showSnackbar(message: string, color: string): void {
|
||||
snackbar.value.message = message;
|
||||
snackbar.value.color = color;
|
||||
snackbar.value.visible = true;
|
||||
}
|
||||
|
||||
// Show the teacher, copying of the code was a successs
|
||||
const copied = ref(false);
|
||||
|
||||
// Copy the generated code to the clipboard
|
||||
async function copyToClipboard(): Promise<void> {
|
||||
await navigator.clipboard.writeText(code.value);
|
||||
copied.value = true;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
<main>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="text-center py-10"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
<style scoped></style>
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="text-center py-10 text-error"
|
||||
>
|
||||
<v-icon large>mdi-alert-circle</v-icon>
|
||||
<p>Error loading: {{ error.message }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ t("classes") }}</h1>
|
||||
<v-container
|
||||
fluid
|
||||
class="ma-4"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
fluid
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="6"
|
||||
>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("classes") }}</th>
|
||||
<th class="header">
|
||||
{{ t("code") }}
|
||||
</th>
|
||||
<th class="header">{{ t("members") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="c in classes"
|
||||
:key="c.id"
|
||||
>
|
||||
<td>
|
||||
<v-btn
|
||||
:to="`/class/${c.id}`"
|
||||
variant="text"
|
||||
>
|
||||
{{ c.displayName }}
|
||||
<v-icon end> mdi-menu-right </v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>{{ c.id }}</td>
|
||||
<td>{{ c.students.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="6"
|
||||
>
|
||||
<div>
|
||||
<h2>{{ t("createClass") }}</h2>
|
||||
|
||||
<v-sheet
|
||||
class="pa-4 sheet"
|
||||
max-width="600px"
|
||||
>
|
||||
<p>{{ t("createClassInstructions") }}</p>
|
||||
<v-form @submit.prevent>
|
||||
<v-text-field
|
||||
class="mt-4"
|
||||
:label="`${t('classname')}`"
|
||||
v-model="className"
|
||||
:placeholder="`${t('EnterNameOfClass')}`"
|
||||
:rules="nameRules"
|
||||
variant="outlined"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
class="mt-4"
|
||||
color="#f6faf2"
|
||||
type="submit"
|
||||
@click="createClass"
|
||||
block
|
||||
>{{ t("create") }}</v-btn
|
||||
>
|
||||
</v-form>
|
||||
</v-sheet>
|
||||
<v-container>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">code</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="code"
|
||||
readonly
|
||||
append-inner-icon="mdi-content-copy"
|
||||
@click:append-inner="copyToClipboard"
|
||||
></v-text-field>
|
||||
<v-slide-y-transition>
|
||||
<div
|
||||
v-if="copied"
|
||||
class="text-center mt-2"
|
||||
>
|
||||
{{ t("copied") }}
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
text
|
||||
@click="
|
||||
dialog = false;
|
||||
copied = false;
|
||||
"
|
||||
>
|
||||
{{ t("close") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<h1 class="title">
|
||||
{{ t("invitations") }}
|
||||
</h1>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("class") }}</th>
|
||||
<th class="header">{{ t("sender") }}</th>
|
||||
<th class="header"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="i in invitations"
|
||||
:key="(i.class as ClassDTO).id"
|
||||
>
|
||||
<td>
|
||||
{{ (i.class as ClassDTO).displayName }}
|
||||
</td>
|
||||
<td>{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}</td>
|
||||
<td class="text-right">
|
||||
<div>
|
||||
<v-btn
|
||||
color="green"
|
||||
@click="acceptRequest"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ t("accept") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="denyRequest"
|
||||
>
|
||||
{{ t("deny") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</div>
|
||||
<v-snackbar
|
||||
v-model="snackbar.visible"
|
||||
:color="snackbar.color"
|
||||
timeout="3000"
|
||||
>
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</main>
|
||||
</template>
|
||||
<style scoped>
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.table thead th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even) {
|
||||
background-color: #f6faf2;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
padding-top: 2%;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0b75bb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import authState from "@/services/auth/auth-service.ts";
|
||||
import TeacherClasses from "./TeacherClasses.vue";
|
||||
import StudentClasses from "./StudentClasses.vue";
|
||||
|
||||
// Determine if role is student or teacher to render correct view
|
||||
const role: string = authState.authState.activeRole!;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
<main>
|
||||
<TeacherClasses v-if="role === 'teacher'"></TeacherClasses>
|
||||
<StudentClasses v-else></StudentClasses>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue