Merge remote-tracking branch 'origin/dev' into feat/endpoints-finaliseren-tests-backend-adriaan
# Conflicts: # backend/src/controllers/assignments.ts # backend/src/controllers/classes.ts # backend/src/controllers/groups.ts # backend/src/controllers/questions.ts # backend/src/controllers/submissions.ts # backend/src/data/assignments/submission-repository.ts # backend/src/data/users/student-repository.ts # backend/src/services/classes.ts # backend/src/services/submissions.ts # backend/tests/controllers/classes.test.ts
This commit is contained in:
commit
ded1a5908e
375 changed files with 23425 additions and 7033 deletions
21
backend/.env.staging
Normal file
21
backend/.env.staging
Normal file
|
@ -0,0 +1,21 @@
|
|||
PORT=3000
|
||||
DWENGO_DB_HOST=db
|
||||
DWENGO_DB_PORT=5432
|
||||
DWENGO_DB_USERNAME=postgres
|
||||
DWENGO_DB_PASSWORD=postgres
|
||||
DWENGO_DB_UPDATE=false
|
||||
|
||||
DWENGO_AUTH_STUDENT_URL=http://localhost/idp/realms/student
|
||||
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs
|
||||
DWENGO_AUTH_TEACHER_URL=http://localhost/idp/realms/teacher
|
||||
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs
|
||||
|
||||
# Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production!
|
||||
#DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/,127.0.0.1:80,http://127.0.0.1,http://localhost:80,http://127.0.0.1:80,localhost
|
||||
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080
|
||||
|
||||
# Logging and monitoring
|
||||
|
||||
LOKI_HOST=http://logging:3102
|
|
@ -1,3 +1,22 @@
|
|||
PORT=3000
|
||||
DWENGO_DB_UPDATE=true
|
||||
#
|
||||
# Test environment configuration
|
||||
#
|
||||
# Should not need to be modified.
|
||||
# See .env.example for more information.
|
||||
#
|
||||
|
||||
### Dwengo ###
|
||||
|
||||
DWENGO_PORT=3000
|
||||
|
||||
DWENGO_DB_NAME=":memory:"
|
||||
DWENGO_DB_UPDATE=true
|
||||
|
||||
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
|
||||
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs
|
||||
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
|
||||
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
|
||||
|
||||
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:9876,*
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#
|
||||
# Test environment configuration
|
||||
#
|
||||
# Should not need to be modified.
|
||||
# See .env.example for more information.
|
||||
#
|
||||
|
||||
### Dwengo ###
|
||||
|
||||
DWENGO_PORT=3000
|
||||
|
||||
DWENGO_DB_NAME=":memory:"
|
||||
DWENGO_DB_UPDATE=true
|
|
@ -1,38 +1,51 @@
|
|||
FROM node:22 AS build-stage
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /app/dwengo
|
||||
|
||||
# Install dependencies
|
||||
|
||||
COPY package*.json ./
|
||||
COPY backend/package.json ./backend/
|
||||
# Backend depends on common and docs
|
||||
COPY common/package.json ./common/
|
||||
COPY docs/package.json ./docs/
|
||||
|
||||
RUN npm install --silent
|
||||
|
||||
# Build the backend
|
||||
|
||||
# Root tsconfig.json
|
||||
COPY tsconfig.json ./
|
||||
COPY tsconfig.json tsconfig.build.json ./
|
||||
|
||||
WORKDIR /app/backend
|
||||
|
||||
COPY backend ./
|
||||
COPY docs /app/docs
|
||||
COPY backend ./backend
|
||||
COPY common ./common
|
||||
COPY docs ./docs
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22 AS production-stage
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /app/dwengo
|
||||
|
||||
COPY package-lock.json backend/package.json ./
|
||||
# Copy static files
|
||||
|
||||
COPY ./backend/i18n ./i18n
|
||||
|
||||
# Copy built files
|
||||
|
||||
COPY --from=build-stage /app/dwengo/common/dist ./common/dist
|
||||
COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist
|
||||
COPY --from=build-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json
|
||||
|
||||
COPY package*.json ./
|
||||
COPY backend/package.json ./backend/
|
||||
# Backend depends on common
|
||||
COPY common/package.json ./common/
|
||||
|
||||
RUN npm install --silent --only=production
|
||||
|
||||
COPY ./docs /docs
|
||||
COPY ./backend/i18n /app/i18n
|
||||
COPY --from=build-stage /app/backend/dist ./dist/
|
||||
COPY ./backend/i18n ./backend/i18n
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "--env-file=.env", "dist/app.js"]
|
||||
CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"]
|
||||
|
|
|
@ -34,7 +34,9 @@ npm run test:unit
|
|||
|
||||
```shell
|
||||
# Omgevingsvariabelen
|
||||
cp .env.development.example .env
|
||||
cp .env.example .env
|
||||
# Configureer de .env file met de juiste waarden!
|
||||
nano .env
|
||||
|
||||
npm run build
|
||||
npm run start
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
// Can be placed in dotenv but found it redundant
|
||||
// Import dotenv from "dotenv";
|
||||
// Load .env file
|
||||
// Dotenv.config();
|
||||
export const DWENGO_API_BASE = 'https://dwengo.org/backend/api';
|
||||
export const FALLBACK_LANG = 'nl';
|
||||
export const FALLBACK_SEQ_NUM = 1;
|
|
@ -8,14 +8,4 @@ export default [
|
|||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ['tests/**/*.ts'],
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,24 +1,28 @@
|
|||
{
|
||||
"name": "dwengo-1-backend",
|
||||
"version": "0.1.1",
|
||||
"name": "@dwengo-1/backend",
|
||||
"version": "0.2.0",
|
||||
"description": "Backend for Dwengo-1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/app.js",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production tsc --project tsconfig.json",
|
||||
"dev": "cross-env NODE_ENV=development tsx watch --env-file=.env.development.local src/app.ts",
|
||||
"build": "cross-env NODE_ENV=production tsc --build",
|
||||
"predev": "tsc --build ../common/tsconfig.json",
|
||||
"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/",
|
||||
"lint": "eslint . --fix",
|
||||
"test:unit": "vitest"
|
||||
"pretest:unit": "tsx ../docs/api/generate.ts && npm run build",
|
||||
"test:unit": "vitest --run",
|
||||
"test:coverage": "vitest --run --coverage.enabled true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mikro-orm/core": "6.4.9",
|
||||
"@mikro-orm/knex": "6.4.9",
|
||||
"@mikro-orm/postgresql": "6.4.9",
|
||||
"@mikro-orm/reflection": "6.4.9",
|
||||
"@mikro-orm/sqlite": "6.4.9",
|
||||
"@mikro-orm/core": "6.4.12",
|
||||
"@mikro-orm/knex": "6.4.12",
|
||||
"@mikro-orm/postgresql": "6.4.12",
|
||||
"@mikro-orm/reflection": "6.4.12",
|
||||
"@mikro-orm/sqlite": "6.4.12",
|
||||
"axios": "^1.8.2",
|
||||
"cors": "^2.8.5",
|
||||
"cross": "^1.0.0",
|
||||
|
@ -33,6 +37,7 @@
|
|||
"jwks-rsa": "^3.1.0",
|
||||
"loki-logger-ts": "^1.0.2",
|
||||
"marked": "^15.0.7",
|
||||
"nanoid": "^5.1.5",
|
||||
"response-time": "^2.3.3",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"uuid": "^11.1.0",
|
||||
|
@ -40,7 +45,7 @@
|
|||
"winston-loki": "^6.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mikro-orm/cli": "6.4.9",
|
||||
"@mikro-orm/cli": "6.4.12",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
|
|
|
@ -5,15 +5,16 @@ import cors from './middleware/cors.js';
|
|||
import { getLogger, Logger } from './logging/initalize.js';
|
||||
import { responseTimeLogger } from './logging/responseTimeLogger.js';
|
||||
import responseTime from 'response-time';
|
||||
import { EnvVars, getNumericEnvVar } from './util/envvars.js';
|
||||
import { envVars, getNumericEnvVar } from './util/envVars.js';
|
||||
import apiRouter from './routes/router.js';
|
||||
import swaggerMiddleware from './swagger.js';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import { errorHandler } from './middleware/error-handling/error-handler.js';
|
||||
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
const app: Express = express();
|
||||
const port: string | number = getNumericEnvVar(EnvVars.Port);
|
||||
const port: string | number = getNumericEnvVar(envVars.Port);
|
||||
|
||||
app.use(express.json());
|
||||
app.use(cors);
|
||||
|
@ -26,7 +27,9 @@ app.use('/api', apiRouter);
|
|||
// Swagger
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
|
||||
|
||||
async function startServer() {
|
||||
app.use(errorHandler);
|
||||
|
||||
async function startServer(): Promise<void> {
|
||||
await initORM();
|
||||
|
||||
app.listen(port, () => {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { EnvVars, getEnvVar } from './util/envvars.js';
|
||||
import { envVars, getEnvVar } from './util/envVars.js';
|
||||
|
||||
// API
|
||||
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
|
||||
export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage);
|
||||
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 });
|
||||
}
|
|
@ -1,72 +1,93 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js';
|
||||
import { AssignmentDTO } from '../interfaces/assignment.js';
|
||||
import {
|
||||
createAssignment,
|
||||
deleteAssignment,
|
||||
getAllAssignments,
|
||||
getAssignment,
|
||||
getAssignmentsQuestions,
|
||||
getAssignmentsSubmissions,
|
||||
putAssignment,
|
||||
} from '../services/assignments.js';
|
||||
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
|
||||
import { requireFields } from './error-helper.js';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
||||
import { EntityDTO } from '@mikro-orm/core';
|
||||
|
||||
// Typescript is annoy with with parameter forwarding from class.ts
|
||||
export async function getAllAssignmentsHandler(req: Request, res: Response): Promise<void> {
|
||||
function getAssignmentParams(req: Request): { classid: string; assignmentNumber: number; full: boolean } {
|
||||
const classid = req.params.classid;
|
||||
const assignmentNumber = Number(req.params.id);
|
||||
const full = req.query.full === 'true';
|
||||
requireFields({ assignmentNumber, classid });
|
||||
|
||||
if (isNaN(assignmentNumber)) {
|
||||
throw new BadRequestException('Assignment id should be a number');
|
||||
}
|
||||
|
||||
return { classid, assignmentNumber, full };
|
||||
}
|
||||
|
||||
export async function getAllAssignmentsHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.classid;
|
||||
const full = req.query.full === 'true';
|
||||
|
||||
const assignments = await getAllAssignments(classid, full);
|
||||
const assignments = await getAllAssignments(classId, full);
|
||||
|
||||
res.json({
|
||||
assignments: assignments,
|
||||
});
|
||||
res.json({ assignments });
|
||||
}
|
||||
|
||||
export async function createAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||
const classid = req.params.classid;
|
||||
const description = req.body.description;
|
||||
const language = req.body.language;
|
||||
const learningPath = req.body.learningPath;
|
||||
const title = req.body.title;
|
||||
|
||||
requireFields({ description, language, learningPath, title });
|
||||
|
||||
const assignmentData = req.body as AssignmentDTO;
|
||||
|
||||
if (!assignmentData.description || !assignmentData.language || !assignmentData.learningPath || !assignmentData.title) {
|
||||
res.status(400).json({
|
||||
error: 'Missing one or more required fields: title, description, learningPath, language',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const assignment = await createAssignment(classid, assignmentData);
|
||||
|
||||
if (!assignment) {
|
||||
res.status(500).json({ error: 'Could not create assignment ' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(201).json(assignment);
|
||||
res.json({ assignment });
|
||||
}
|
||||
|
||||
export async function getAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||
const id = +req.params.id;
|
||||
const classid = req.params.classid;
|
||||
const { classid, assignmentNumber } = getAssignmentParams(req);
|
||||
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||
return;
|
||||
}
|
||||
const assignment = await getAssignment(classid, assignmentNumber);
|
||||
|
||||
const assignment = await getAssignment(classid, id);
|
||||
res.json({ assignment });
|
||||
}
|
||||
|
||||
if (!assignment) {
|
||||
res.status(404).json({ error: 'Assignment not found' });
|
||||
return;
|
||||
}
|
||||
export async function putAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||
const { classid, assignmentNumber } = getAssignmentParams(req);
|
||||
|
||||
res.json(assignment);
|
||||
const assignmentData = req.body as Partial<EntityDTO<Assignment>>;
|
||||
const assignment = await putAssignment(classid, assignmentNumber, assignmentData);
|
||||
|
||||
res.json({ assignment });
|
||||
}
|
||||
|
||||
export async function deleteAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||
const { classid, assignmentNumber } = getAssignmentParams(req);
|
||||
|
||||
const assignment = await deleteAssignment(classid, assignmentNumber);
|
||||
|
||||
res.json({ assignment });
|
||||
}
|
||||
|
||||
export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const classid = req.params.classid;
|
||||
const assignmentNumber = +req.params.id;
|
||||
const full = req.query.full === 'true';
|
||||
|
||||
if (isNaN(assignmentNumber)) {
|
||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||
return;
|
||||
}
|
||||
const { classid, assignmentNumber, full } = getAssignmentParams(req);
|
||||
|
||||
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full);
|
||||
|
||||
res.json({
|
||||
submissions: submissions,
|
||||
});
|
||||
res.json({ submissions });
|
||||
}
|
||||
|
||||
export async function getAssignmentQuestionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const { classid, assignmentNumber, full } = getAssignmentParams(req);
|
||||
|
||||
const questions = await getAssignmentsQuestions(classid, assignmentNumber, full);
|
||||
|
||||
res.json({ questions });
|
||||
}
|
||||
|
|
|
@ -1,33 +1,62 @@
|
|||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
||||
import { UnauthorizedException } from '../exceptions/unauthorized-exception.js';
|
||||
import { getLogger } from '../logging/initalize.js';
|
||||
import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js';
|
||||
import { createOrUpdateStudent } from '../services/students.js';
|
||||
import { createOrUpdateTeacher } from '../services/teachers.js';
|
||||
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||
import { Response } from 'express';
|
||||
|
||||
type FrontendIdpConfig = {
|
||||
interface FrontendIdpConfig {
|
||||
authority: string;
|
||||
clientId: string;
|
||||
scope: string;
|
||||
responseType: string;
|
||||
};
|
||||
}
|
||||
|
||||
type FrontendAuthConfig = {
|
||||
interface FrontendAuthConfig {
|
||||
student: FrontendIdpConfig;
|
||||
teacher: FrontendIdpConfig;
|
||||
};
|
||||
}
|
||||
|
||||
const SCOPE = 'openid profile email';
|
||||
const RESPONSE_TYPE = 'code';
|
||||
|
||||
const logger = getLogger();
|
||||
|
||||
export function getFrontendAuthConfig(): FrontendAuthConfig {
|
||||
return {
|
||||
student: {
|
||||
authority: getEnvVar(EnvVars.IdpStudentUrl),
|
||||
clientId: getEnvVar(EnvVars.IdpStudentClientId),
|
||||
authority: getEnvVar(envVars.IdpStudentUrl),
|
||||
clientId: getEnvVar(envVars.IdpStudentClientId),
|
||||
scope: SCOPE,
|
||||
responseType: RESPONSE_TYPE,
|
||||
},
|
||||
teacher: {
|
||||
authority: getEnvVar(EnvVars.IdpTeacherUrl),
|
||||
clientId: getEnvVar(EnvVars.IdpTeacherClientId),
|
||||
authority: getEnvVar(envVars.IdpTeacherUrl),
|
||||
clientId: getEnvVar(envVars.IdpTeacherClientId),
|
||||
scope: SCOPE,
|
||||
responseType: RESPONSE_TYPE,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
const auth = req.auth;
|
||||
if (!auth) {
|
||||
throw new UnauthorizedException('Cannot say hello when not authenticated.');
|
||||
}
|
||||
const userData = {
|
||||
id: auth.username,
|
||||
username: auth.username,
|
||||
firstName: auth.firstName ?? '',
|
||||
lastName: auth.lastName ?? '',
|
||||
};
|
||||
if (auth.accountType === 'student') {
|
||||
await createOrUpdateStudent(userData);
|
||||
logger.debug(`Synchronized student ${userData.username} with IDP`);
|
||||
} else {
|
||||
await createOrUpdateTeacher(userData);
|
||||
logger.debug(`Synchronized teacher ${userData.username} with IDP`);
|
||||
}
|
||||
res.status(200).send({ message: 'Welcome!' });
|
||||
}
|
||||
|
|
|
@ -1,76 +1,132 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { createClass, getAllClasses, getClass, getClassStudents, getClassTeacherInvitations } from '../services/class.js';
|
||||
import { ClassDTO } from '../interfaces/class.js';
|
||||
import {
|
||||
addClassStudent,
|
||||
addClassTeacher,
|
||||
createClass,
|
||||
deleteClass,
|
||||
deleteClassStudent,
|
||||
deleteClassTeacher,
|
||||
getAllClasses,
|
||||
getClass,
|
||||
getClassStudents,
|
||||
getClassTeacherInvitations,
|
||||
getClassTeachers,
|
||||
putClass,
|
||||
} from '../services/classes.js';
|
||||
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
|
||||
import { requireFields } from './error-helper.js';
|
||||
import { EntityDTO } from '@mikro-orm/core';
|
||||
import { Class } from '../entities/classes/class.entity.js';
|
||||
|
||||
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
|
||||
const full = req.query.full === 'true';
|
||||
const classes = await getAllClasses(full);
|
||||
|
||||
res.json({
|
||||
classes: classes,
|
||||
});
|
||||
res.json({ classes });
|
||||
}
|
||||
|
||||
export async function createClassHandler(req: Request, res: Response): Promise<void> {
|
||||
const displayName = req.body.displayName;
|
||||
requireFields({ displayName });
|
||||
|
||||
const classData = req.body as ClassDTO;
|
||||
|
||||
if (!classData.displayName) {
|
||||
res.status(400).json({
|
||||
error: 'Missing one or more required fields: displayName',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const cls = await createClass(classData);
|
||||
|
||||
if (!cls) {
|
||||
res.status(500).json({ error: 'Something went wrong while creating class' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(201).json(cls);
|
||||
res.json({ class: cls });
|
||||
}
|
||||
|
||||
export async function getClassHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.id;
|
||||
requireFields({ classId });
|
||||
|
||||
const cls = await getClass(classId);
|
||||
|
||||
if (!cls) {
|
||||
res.status(404).json({ error: 'Class not found' });
|
||||
return;
|
||||
}
|
||||
res.json({ class: cls });
|
||||
}
|
||||
|
||||
res.json(cls);
|
||||
export async function putClassHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.id;
|
||||
requireFields({ classId });
|
||||
|
||||
const newData = req.body as Partial<EntityDTO<Class>>;
|
||||
const cls = await putClass(classId, newData);
|
||||
|
||||
res.json({ class: cls });
|
||||
}
|
||||
|
||||
export async function deleteClassHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.id;
|
||||
const cls = await deleteClass(classId);
|
||||
|
||||
res.json({ class: cls });
|
||||
}
|
||||
|
||||
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.id;
|
||||
const full = req.query.full === 'true';
|
||||
requireFields({ classId });
|
||||
|
||||
const students = await getClassStudents(classId, full);
|
||||
|
||||
if (!students) {
|
||||
res.status(404).json({ error: 'Class not found' });
|
||||
return;
|
||||
}
|
||||
res.json({ students });
|
||||
}
|
||||
|
||||
res.json({
|
||||
students: students,
|
||||
});
|
||||
export async function getClassTeachersHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.id;
|
||||
const full = req.query.full === 'true';
|
||||
requireFields({ classId });
|
||||
|
||||
const teachers = await getClassTeachers(classId, full);
|
||||
|
||||
res.json({ teachers });
|
||||
}
|
||||
|
||||
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.id;
|
||||
const full = req.query.full === 'true';
|
||||
requireFields({ classId });
|
||||
|
||||
const invitations = await getClassTeacherInvitations(classId, full);
|
||||
|
||||
if (!invitations) {
|
||||
res.status(404).json({ error: 'Class not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
invitations: invitations,
|
||||
});
|
||||
res.json({ invitations });
|
||||
}
|
||||
|
||||
export async function deleteClassStudentHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.id;
|
||||
const username = req.params.username;
|
||||
requireFields({ classId, username });
|
||||
|
||||
const cls = await deleteClassStudent(classId, username);
|
||||
|
||||
res.json({ class: cls });
|
||||
}
|
||||
|
||||
export async function deleteClassTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.id;
|
||||
const username = req.params.username;
|
||||
requireFields({ classId, username });
|
||||
|
||||
const cls = await deleteClassTeacher(classId, username);
|
||||
|
||||
res.json({ class: cls });
|
||||
}
|
||||
|
||||
export async function addClassStudentHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.id;
|
||||
const username = req.body.username;
|
||||
requireFields({ classId, username });
|
||||
|
||||
const cls = await addClassStudent(classId, username);
|
||||
|
||||
res.json({ class: cls });
|
||||
}
|
||||
|
||||
export async function addClassTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.id;
|
||||
const username = req.body.username;
|
||||
requireFields({ classId, username });
|
||||
|
||||
const cls = await addClassTeacher(classId, username);
|
||||
|
||||
res.json({ class: cls });
|
||||
}
|
||||
|
|
18
backend/src/controllers/error-helper.ts
Normal file
18
backend/src/controllers/error-helper.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
|
||||
/**
|
||||
* Checks for the presence of required fields and throws a BadRequestException
|
||||
* if any are missing.
|
||||
*
|
||||
* @param fields - An object with key-value pairs to validate.
|
||||
*/
|
||||
export function requireFields(fields: Record<string, unknown>): void {
|
||||
const missing = Object.entries(fields)
|
||||
.filter(([_, value]) => value === undefined || value === null || value === '')
|
||||
.map(([key]) => key);
|
||||
|
||||
if (missing.length > 0) {
|
||||
const message = `Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`;
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
}
|
|
@ -1,93 +1,120 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js';
|
||||
import { GroupDTO } from '../interfaces/group.js';
|
||||
import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupQuestions, getGroupSubmissions, putGroup } from '../services/groups.js';
|
||||
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
|
||||
import { requireFields } from './error-helper.js';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
|
||||
function checkGroupFields(classId: string, assignmentId: number, groupId: number): void {
|
||||
requireFields({ classId, assignmentId, groupId });
|
||||
|
||||
if (isNaN(assignmentId)) {
|
||||
throw new BadRequestException('Assignment id must be a number');
|
||||
}
|
||||
|
||||
if (isNaN(groupId)) {
|
||||
throw new BadRequestException('Group id must be a number');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGroupHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.classid;
|
||||
const full = req.query.full === 'true';
|
||||
const assignmentId = +req.params.assignmentid;
|
||||
const assignmentId = parseInt(req.params.assignmentid);
|
||||
const groupId = parseInt(req.params.groupid);
|
||||
checkGroupFields(classId, assignmentId, groupId);
|
||||
|
||||
if (isNaN(assignmentId)) {
|
||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||
return;
|
||||
}
|
||||
const group = await getGroup(classId, assignmentId, groupId);
|
||||
|
||||
const groupId = +req.params.groupid!; // Can't be undefined
|
||||
res.json({ group });
|
||||
}
|
||||
|
||||
if (isNaN(groupId)) {
|
||||
res.status(400).json({ error: 'Group id must be a number' });
|
||||
return;
|
||||
}
|
||||
export async function putGroupHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.classid;
|
||||
const assignmentId = parseInt(req.params.assignmentid);
|
||||
const groupId = parseInt(req.params.groupid);
|
||||
checkGroupFields(classId, assignmentId, groupId);
|
||||
|
||||
const group = await getGroup(classId, assignmentId, groupId, full);
|
||||
// Only members field can be changed
|
||||
const members = req.body.members;
|
||||
requireFields({ members });
|
||||
|
||||
if (!group) {
|
||||
res.status(404).json({ error: 'Group not found' });
|
||||
return;
|
||||
}
|
||||
const group = await putGroup(classId, assignmentId, groupId, { members } as Partial<GroupDTO>);
|
||||
|
||||
res.json(group);
|
||||
res.json({ group });
|
||||
}
|
||||
|
||||
export async function deleteGroupHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.classid;
|
||||
const assignmentId = parseInt(req.params.assignmentid);
|
||||
const groupId = parseInt(req.params.groupid);
|
||||
checkGroupFields(classId, assignmentId, groupId);
|
||||
|
||||
const group = await deleteGroup(classId, assignmentId, groupId);
|
||||
|
||||
res.json({ group });
|
||||
}
|
||||
|
||||
export async function getAllGroupsHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.classid;
|
||||
const assignmentId = Number(req.params.assignmentid);
|
||||
const full = req.query.full === 'true';
|
||||
|
||||
const assignmentId = +req.params.assignmentid;
|
||||
requireFields({ classId, assignmentId });
|
||||
|
||||
if (isNaN(assignmentId)) {
|
||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||
return;
|
||||
throw new BadRequestException('Assignment id must be a number');
|
||||
}
|
||||
|
||||
const groups = await getAllGroups(classId, assignmentId, full);
|
||||
|
||||
res.json({
|
||||
groups: groups,
|
||||
});
|
||||
res.json({ groups });
|
||||
}
|
||||
|
||||
export async function createGroupHandler(req: Request, res: Response): Promise<void> {
|
||||
const classid = req.params.classid;
|
||||
const assignmentId = +req.params.assignmentid;
|
||||
const assignmentId = Number(req.params.assignmentid);
|
||||
const members = req.body.members;
|
||||
requireFields({ classid, assignmentId, members });
|
||||
|
||||
if (isNaN(assignmentId)) {
|
||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||
return;
|
||||
throw new BadRequestException('Assignment id must be a number');
|
||||
}
|
||||
|
||||
const groupData = req.body as GroupDTO;
|
||||
const group = await createGroup(groupData, classid, assignmentId);
|
||||
|
||||
if (!group) {
|
||||
res.status(500).json({ error: 'Something went wrong while creating group' });
|
||||
return;
|
||||
res.status(201).json({ group });
|
||||
}
|
||||
|
||||
function getGroupParams(req: Request): { classId: string; assignmentId: number; groupId: number; full: boolean } {
|
||||
const classId = req.params.classid;
|
||||
const assignmentId = Number(req.params.assignmentid);
|
||||
const groupId = Number(req.params.groupid);
|
||||
const full = req.query.full === 'true';
|
||||
|
||||
requireFields({ classId, assignmentId, groupId });
|
||||
|
||||
if (isNaN(assignmentId)) {
|
||||
throw new BadRequestException('Assignment id must be a number');
|
||||
}
|
||||
|
||||
res.status(201).json(group);
|
||||
if (isNaN(groupId)) {
|
||||
throw new BadRequestException('Group id must be a number');
|
||||
}
|
||||
|
||||
return { classId, assignmentId, groupId, full };
|
||||
}
|
||||
|
||||
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.classid;
|
||||
const full = req.query.full === 'true';
|
||||
|
||||
const assignmentId = +req.params.assignmentid;
|
||||
|
||||
if (isNaN(assignmentId)) {
|
||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||
return;
|
||||
}
|
||||
|
||||
const groupId = +req.params.groupid!; // Can't be undefined
|
||||
|
||||
if (isNaN(groupId)) {
|
||||
res.status(400).json({ error: 'Group id must be a number' });
|
||||
return;
|
||||
}
|
||||
const { classId, assignmentId, groupId, full } = getGroupParams(req);
|
||||
|
||||
const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full);
|
||||
|
||||
res.json({
|
||||
submissions: submissions,
|
||||
});
|
||||
res.json({ submissions });
|
||||
}
|
||||
|
||||
export async function getGroupQuestionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const { classId, assignmentId, groupId, full } = getGroupParams(req);
|
||||
|
||||
const questions = await getGroupQuestions(classId, assignmentId, groupId, full);
|
||||
|
||||
res.json({ questions });
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { FALLBACK_LANG } from '../config.js';
|
||||
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js';
|
||||
import learningObjectService from '../services/learning-objects/learning-object-service.js';
|
||||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
||||
import { Language } from '../entities/content/language.js';
|
||||
import { BadRequestException } from '../exceptions.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import attachmentService from '../services/learning-objects/attachment-service.js';
|
||||
import { NotFoundError } from '@mikro-orm/core';
|
||||
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, 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.');
|
||||
}
|
||||
return {
|
||||
hruid: req.params.hruid as string,
|
||||
language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language,
|
||||
hruid: req.params.hruid,
|
||||
language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language,
|
||||
version: parseInt(req.query.version as string),
|
||||
};
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif
|
|||
throw new BadRequestException('HRUID is required.');
|
||||
}
|
||||
return {
|
||||
hruid: req.params.hruid as string,
|
||||
hruid: req.params.hruid,
|
||||
language: (req.query.language as Language) || FALLBACK_LANG,
|
||||
};
|
||||
}
|
||||
|
@ -47,6 +47,11 @@ export async function getLearningObject(req: Request, res: Response): Promise<vo
|
|||
const learningObjectId = getLearningObjectIdentifierFromRequest(req);
|
||||
|
||||
const learningObject = await learningObjectService.getLearningObjectById(learningObjectId);
|
||||
|
||||
if (!learningObject) {
|
||||
throw new NotFoundException('Learning object not found');
|
||||
}
|
||||
|
||||
res.json(learningObject);
|
||||
}
|
||||
|
||||
|
@ -63,7 +68,7 @@ export async function getAttachment(req: Request, res: Response): Promise<void>
|
|||
const attachment = await attachmentService.getAttachment(learningObjectId, name);
|
||||
|
||||
if (!attachment) {
|
||||
throw new NotFoundError(`Attachment ${name} not found`);
|
||||
throw new NotFoundException(`Attachment ${name} not found`);
|
||||
}
|
||||
res.setHeader('Content-Type', attachment.mimeType).send(attachment.content);
|
||||
}
|
||||
|
|
|
@ -2,13 +2,11 @@ import { Request, Response } from 'express';
|
|||
import { themes } from '../data/themes.js';
|
||||
import { FALLBACK_LANG } from '../config.js';
|
||||
import learningPathService from '../services/learning-paths/learning-path-service.js';
|
||||
import { BadRequestException, NotFoundException } from '../exceptions.js';
|
||||
import { Language } from '../entities/content/language.js';
|
||||
import {
|
||||
PersonalizationTarget,
|
||||
personalizedForGroup,
|
||||
personalizedForStudent,
|
||||
} from '../services/learning-paths/learning-path-personalization-util.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||
import { Group } from '../entities/assignments/group.entity.js';
|
||||
import { getAssignmentRepository, getGroupRepository } from '../data/repositories.js';
|
||||
|
||||
/**
|
||||
* Fetch learning paths based on query parameters.
|
||||
|
@ -19,20 +17,20 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
|
|||
const searchQuery = req.query.search as string;
|
||||
const language = (req.query.language as string) || FALLBACK_LANG;
|
||||
|
||||
const forStudent = req.query.forStudent as string;
|
||||
const forGroupNo = req.query.forGroup as string;
|
||||
const assignmentNo = req.query.assignmentNo as string;
|
||||
const classId = req.query.classId as string;
|
||||
|
||||
let personalizationTarget: PersonalizationTarget | undefined;
|
||||
let forGroup: Group | undefined;
|
||||
|
||||
if (forStudent) {
|
||||
personalizationTarget = await personalizedForStudent(forStudent);
|
||||
} else if (forGroupNo) {
|
||||
if (forGroupNo) {
|
||||
if (!assignmentNo || !classId) {
|
||||
throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.');
|
||||
}
|
||||
personalizationTarget = await personalizedForGroup(classId, parseInt(assignmentNo), parseInt(forGroupNo));
|
||||
const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo));
|
||||
if (assignment) {
|
||||
forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let hruidList;
|
||||
|
@ -47,18 +45,13 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
|
|||
throw new NotFoundException(`Theme "${themeKey}" not found.`);
|
||||
}
|
||||
} else if (searchQuery) {
|
||||
const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, personalizationTarget);
|
||||
const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup);
|
||||
res.json(searchResults);
|
||||
return;
|
||||
} else {
|
||||
hruidList = themes.flatMap((theme) => theme.hruids);
|
||||
}
|
||||
|
||||
const learningPaths = await learningPathService.fetchLearningPaths(
|
||||
hruidList,
|
||||
language as Language,
|
||||
`HRUIDs: ${hruidList.join(', ')}`,
|
||||
personalizationTarget
|
||||
);
|
||||
const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup);
|
||||
res.json(learningPaths.data);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,27 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js';
|
||||
import { QuestionDTO, QuestionId } from '../interfaces/question.js';
|
||||
import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js';
|
||||
import {
|
||||
createQuestion,
|
||||
deleteQuestion,
|
||||
getAllQuestions,
|
||||
getQuestion,
|
||||
getQuestionsAboutLearningObjectInAssignment,
|
||||
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 { Language } from '../entities/content/language.js';
|
||||
|
||||
function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null {
|
||||
const { hruid, version } = req.params;
|
||||
const lang = req.query.lang;
|
||||
|
||||
if (!hruid || !version) {
|
||||
res.status(400).json({ error: 'Missing required parameters.' });
|
||||
return null;
|
||||
}
|
||||
import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { requireFields } from './error-helper.js';
|
||||
|
||||
export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier {
|
||||
return {
|
||||
hruid,
|
||||
language: (lang as Language) || FALLBACK_LANG,
|
||||
version: +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,85 +29,96 @@ 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 ? req.query.lang : FALLBACK_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);
|
||||
|
||||
if (!questions) {
|
||||
res.status(404).json({ error: `Questions not found.` });
|
||||
let questions: QuestionDTO[] | QuestionId[];
|
||||
if (req.query.classId && req.query.assignmentId) {
|
||||
questions = await getQuestionsAboutLearningObjectInAssignment(
|
||||
learningObjectId,
|
||||
req.query.classId as string,
|
||||
parseInt(req.query.assignmentId as string),
|
||||
full ?? false,
|
||||
req.query.forStudent as string | undefined
|
||||
);
|
||||
} else {
|
||||
res.json({ questions: questions });
|
||||
questions = await getAllQuestions(learningObjectId, full ?? false);
|
||||
}
|
||||
|
||||
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;
|
||||
const inGroup = req.body.inGroup;
|
||||
requireFields({ author, content, inGroup });
|
||||
|
||||
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) {
|
||||
res.json(404).json({ error: 'Question not found' });
|
||||
return;
|
||||
}
|
||||
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||
const questionId = getQuestionId(learningObjectId, seq);
|
||||
|
||||
const question = await deleteQuestion(questionId);
|
||||
|
||||
if (!question) {
|
||||
res.status(404).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 });
|
||||
}
|
||||
|
|
|
@ -1,101 +1,67 @@
|
|||
import { Request, Response } from 'express';
|
||||
import {
|
||||
createClassJoinRequest,
|
||||
createStudent,
|
||||
deleteClassJoinRequest,
|
||||
deleteStudent,
|
||||
getAllStudents,
|
||||
getJoinRequestByStudentClass,
|
||||
getJoinRequestsByStudent,
|
||||
getStudent,
|
||||
getStudentAssignments,
|
||||
getStudentClasses,
|
||||
getStudentGroups,
|
||||
getStudentQuestions,
|
||||
getStudentSubmissions,
|
||||
} from '../services/students.js';
|
||||
import { StudentDTO } from '../interfaces/student.js';
|
||||
import { requireFields } from './error-helper.js';
|
||||
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||
|
||||
// TODO: accept arguments (full, ...)
|
||||
// TODO: endpoints
|
||||
export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> {
|
||||
const full = req.query.full === 'true';
|
||||
|
||||
const students = await getAllStudents(full);
|
||||
const students: StudentDTO[] | string[] = await getAllStudents(full);
|
||||
|
||||
if (!students) {
|
||||
res.status(404).json({ error: `Student not found.` });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ students: students });
|
||||
res.json({ students });
|
||||
}
|
||||
|
||||
export async function getStudentHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
requireFields({ username });
|
||||
|
||||
if (!username) {
|
||||
res.status(400).json({ error: 'Missing required field: username' });
|
||||
return;
|
||||
}
|
||||
const student = await getStudent(username);
|
||||
|
||||
const user = await getStudent(username);
|
||||
|
||||
if (!user) {
|
||||
res.status(404).json({
|
||||
error: `User with username '${username}' not found.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(user);
|
||||
res.json({ student });
|
||||
}
|
||||
|
||||
export async function createStudentHandler(req: Request, res: Response) {
|
||||
export async function createStudentHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.body.username;
|
||||
const firstName = req.body.firstName;
|
||||
const lastName = req.body.lastName;
|
||||
requireFields({ username, firstName, lastName });
|
||||
|
||||
const userData = req.body as StudentDTO;
|
||||
|
||||
if (!userData.username || !userData.firstName || !userData.lastName) {
|
||||
res.status(400).json({
|
||||
error: 'Missing required fields: username, firstName, lastName',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newUser = await createStudent(userData);
|
||||
|
||||
if (!newUser) {
|
||||
res.status(500).json({
|
||||
error: 'Something went wrong while creating student'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(201).json(newUser);
|
||||
const student = await createStudent(userData);
|
||||
res.json({ student });
|
||||
}
|
||||
|
||||
export async function deleteStudentHandler(req: Request, res: Response) {
|
||||
export async function deleteStudentHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
requireFields({ username });
|
||||
|
||||
if (!username) {
|
||||
res.status(400).json({ error: 'Missing required field: username' });
|
||||
return;
|
||||
}
|
||||
|
||||
const deletedUser = await deleteStudent(username);
|
||||
if (!deletedUser) {
|
||||
res.status(404).json({
|
||||
error: `User with username '${username}' not found.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json(deletedUser);
|
||||
const student = await deleteStudent(username);
|
||||
res.json({ student });
|
||||
}
|
||||
|
||||
export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> {
|
||||
const full = req.query.full === 'true';
|
||||
const username = req.params.id;
|
||||
const username = req.params.username;
|
||||
requireFields({ username });
|
||||
|
||||
const classes = await getStudentClasses(username, full);
|
||||
|
||||
res.json({
|
||||
classes: classes,
|
||||
});
|
||||
res.json({ classes });
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
@ -104,33 +70,75 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro
|
|||
// Have this assignment.
|
||||
export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> {
|
||||
const full = req.query.full === 'true';
|
||||
const username = req.params.id;
|
||||
const username = req.params.username;
|
||||
requireFields({ username });
|
||||
|
||||
const assignments = getStudentAssignments(username, full);
|
||||
const assignments = await getStudentAssignments(username, full);
|
||||
|
||||
res.json({
|
||||
assignments: assignments,
|
||||
});
|
||||
res.json({ assignments });
|
||||
}
|
||||
|
||||
export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> {
|
||||
const full = req.query.full === 'true';
|
||||
const username = req.params.id;
|
||||
const username = req.params.username;
|
||||
requireFields({ username });
|
||||
|
||||
const groups = await getStudentGroups(username, full);
|
||||
|
||||
res.json({
|
||||
groups: groups,
|
||||
});
|
||||
res.json({ groups });
|
||||
}
|
||||
|
||||
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.id;
|
||||
const username = req.params.username;
|
||||
const full = req.query.full === 'true';
|
||||
requireFields({ username });
|
||||
|
||||
const submissions = await getStudentSubmissions(username, full);
|
||||
|
||||
res.json({
|
||||
submissions: submissions,
|
||||
});
|
||||
res.json({ submissions });
|
||||
}
|
||||
|
||||
export async function getStudentQuestionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const full = req.query.full === 'true';
|
||||
const username = req.params.username;
|
||||
requireFields({ username });
|
||||
|
||||
const questions = await getStudentQuestions(username, full);
|
||||
|
||||
res.json({ questions });
|
||||
}
|
||||
|
||||
export async function createStudentRequestHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
const classId = req.body.classId;
|
||||
requireFields({ username, classId });
|
||||
|
||||
const request = await createClassJoinRequest(username, classId);
|
||||
res.json({ request });
|
||||
}
|
||||
|
||||
export async function getStudentRequestsHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
requireFields({ username });
|
||||
|
||||
const requests = await getJoinRequestsByStudent(username);
|
||||
res.json({ requests });
|
||||
}
|
||||
|
||||
export async function getStudentRequestHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
const classId = req.params.classId;
|
||||
requireFields({ username, classId });
|
||||
|
||||
const request = await getJoinRequestByStudentClass(username, classId);
|
||||
res.json({ request });
|
||||
}
|
||||
|
||||
export async function deleteClassJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
const classId = req.params.classId;
|
||||
requireFields({ username, classId });
|
||||
|
||||
const request = await deleteClassJoinRequest(username, classId);
|
||||
res.json({ request });
|
||||
}
|
||||
|
|
|
@ -1,68 +1,86 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { createSubmission, deleteSubmission, getAllSubmissions, getSubmission } from '../services/submissions.js';
|
||||
import { Language, languageMap } from '../entities/content/language.js';
|
||||
import { SubmissionDTO } from '../interfaces/submission';
|
||||
import {
|
||||
createSubmission,
|
||||
deleteSubmission,
|
||||
getAllSubmissions,
|
||||
getSubmission,
|
||||
getSubmissionsForLearningObjectAndAssignment,
|
||||
} from '../services/submissions.js';
|
||||
import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission';
|
||||
import { Language, languageMap } from '@dwengo-1/common/util/language';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
import { requireFields } from './error-helper.js';
|
||||
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||
|
||||
export async function getSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const loHruid = req.params.hruid;
|
||||
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
||||
const version = parseInt(req.query.version as string) ?? 1;
|
||||
|
||||
const forGroup = req.query.forGroup as string | undefined;
|
||||
|
||||
const submissions: SubmissionDTO[] = await getSubmissionsForLearningObjectAndAssignment(
|
||||
loHruid,
|
||||
lang,
|
||||
version,
|
||||
req.query.classId as string,
|
||||
parseInt(req.query.assignmentId as string),
|
||||
forGroup ? parseInt(forGroup) : undefined
|
||||
);
|
||||
|
||||
res.json({ submissions });
|
||||
}
|
||||
|
||||
export async function getSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||
const lohruid = req.params.hruid;
|
||||
const submissionNumber = +req.params.id;
|
||||
|
||||
if (isNaN(submissionNumber)) {
|
||||
res.status(400).json({ error: 'Submission number is not a number' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
||||
const version = (req.query.version || 1) as number;
|
||||
const submissionNumber = Number(req.params.id);
|
||||
requireFields({ lohruid, submissionNumber });
|
||||
|
||||
const submission = await getSubmission(lohruid, lang, version, submissionNumber);
|
||||
|
||||
if (!submission) {
|
||||
res.status(404).json({ error: 'Submission not found' });
|
||||
return;
|
||||
if (isNaN(submissionNumber)) {
|
||||
throw new BadRequestException('Submission number must be a number');
|
||||
}
|
||||
|
||||
res.json(submission);
|
||||
const loId = new LearningObjectIdentifier(lohruid, lang, version);
|
||||
const submission = await getSubmission(loId, submissionNumber);
|
||||
|
||||
res.json({ submission });
|
||||
}
|
||||
|
||||
export async function getAllSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const lohruid = req.params.hruid;
|
||||
|
||||
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
||||
const version = (req.query.version || 1) as number;
|
||||
requireFields({ lohruid });
|
||||
|
||||
const submissions = await getAllSubmissions(lohruid, lang, version);
|
||||
const loId = new LearningObjectIdentifier(lohruid, lang, version);
|
||||
const submissions = await getAllSubmissions(loId);
|
||||
|
||||
res.json({ submissions: submissions });
|
||||
res.json({ submissions });
|
||||
}
|
||||
|
||||
export async function createSubmissionHandler(req: Request, res: Response) {
|
||||
// TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
|
||||
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||
const submissionDTO = req.body as SubmissionDTO;
|
||||
|
||||
const submission = await createSubmission(submissionDTO);
|
||||
|
||||
if (!submission) {
|
||||
res.status(400).json({ error: 'Failed to create submission' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(submission);
|
||||
res.json({ submission });
|
||||
}
|
||||
|
||||
export async function deleteSubmissionHandler(req: Request, res: Response) {
|
||||
export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||
const hruid = req.params.hruid;
|
||||
const submissionNumber = +req.params.id;
|
||||
|
||||
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
||||
const version = (req.query.version || 1) as number;
|
||||
const submissionNumber = Number(req.params.id);
|
||||
requireFields({ hruid, submissionNumber });
|
||||
|
||||
const submission = await deleteSubmission(hruid, lang, version, submissionNumber);
|
||||
|
||||
if (!submission) {
|
||||
res.status(404).json({ error: 'Submission not found' });
|
||||
return;
|
||||
if (isNaN(submissionNumber)) {
|
||||
throw new BadRequestException('Submission number must be a number');
|
||||
}
|
||||
|
||||
res.json(submission);
|
||||
const loId = new LearningObjectIdentifier(hruid, lang, version);
|
||||
const submission = await deleteSubmission(loId, submissionNumber);
|
||||
|
||||
res.json({ submission });
|
||||
}
|
||||
|
|
66
backend/src/controllers/teacher-invitations.ts
Normal file
66
backend/src/controllers/teacher-invitations.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { requireFields } from './error-helper.js';
|
||||
import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js';
|
||||
import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation';
|
||||
|
||||
export async function getAllInvitationsHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
const by = req.query.sent === 'true';
|
||||
requireFields({ username });
|
||||
|
||||
const invitations = await getAllInvitations(username, by);
|
||||
|
||||
res.json({ invitations });
|
||||
}
|
||||
|
||||
export async function getInvitationHandler(req: Request, res: Response): Promise<void> {
|
||||
const sender = req.params.sender;
|
||||
const receiver = req.params.receiver;
|
||||
const classId = req.params.classId;
|
||||
requireFields({ sender, receiver, classId });
|
||||
|
||||
const invitation = await getInvitation(sender, receiver, classId);
|
||||
|
||||
res.json({ invitation });
|
||||
}
|
||||
|
||||
export async function createInvitationHandler(req: Request, res: Response): Promise<void> {
|
||||
const sender = req.body.sender;
|
||||
const receiver = req.body.receiver;
|
||||
const classId = req.body.class;
|
||||
requireFields({ sender, receiver, classId });
|
||||
|
||||
const data = req.body as TeacherInvitationData;
|
||||
const invitation = await createInvitation(data);
|
||||
|
||||
res.json({ invitation });
|
||||
}
|
||||
|
||||
export async function updateInvitationHandler(req: Request, res: Response): Promise<void> {
|
||||
const sender = req.body.sender;
|
||||
const receiver = req.body.receiver;
|
||||
const classId = req.body.class;
|
||||
req.body.accepted = req.body.accepted !== false;
|
||||
requireFields({ sender, receiver, classId });
|
||||
|
||||
const data = req.body as TeacherInvitationData;
|
||||
const invitation = await updateInvitation(data);
|
||||
|
||||
res.json({ invitation });
|
||||
}
|
||||
|
||||
export async function deleteInvitationHandler(req: Request, res: Response): Promise<void> {
|
||||
const sender = req.params.sender;
|
||||
const receiver = req.params.receiver;
|
||||
const classId = req.params.classId;
|
||||
requireFields({ sender, receiver, classId });
|
||||
|
||||
const data: TeacherInvitationData = {
|
||||
sender,
|
||||
receiver,
|
||||
class: classId,
|
||||
};
|
||||
const invitation = await deleteInvitation(data);
|
||||
|
||||
res.json({ invitation });
|
||||
}
|
|
@ -4,137 +4,96 @@ import {
|
|||
deleteTeacher,
|
||||
getAllTeachers,
|
||||
getClassesByTeacher,
|
||||
getQuestionsByTeacher,
|
||||
getJoinRequestsByClass,
|
||||
getStudentsByTeacher,
|
||||
getTeacher,
|
||||
getTeacherQuestions,
|
||||
updateClassJoinRequestStatus,
|
||||
} from '../services/teachers.js';
|
||||
import { TeacherDTO } from '../interfaces/teacher.js';
|
||||
import { requireFields } from './error-helper.js';
|
||||
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
||||
|
||||
export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> {
|
||||
const full = req.query.full === 'true';
|
||||
|
||||
const teachers = await getAllTeachers(full);
|
||||
const teachers: TeacherDTO[] | string[] = await getAllTeachers(full);
|
||||
|
||||
if (!teachers) {
|
||||
res.status(404).json({ error: `Teacher not found.` });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ teachers: teachers });
|
||||
res.json({ teachers });
|
||||
}
|
||||
|
||||
export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
requireFields({ username });
|
||||
|
||||
if (!username) {
|
||||
res.status(400).json({ error: 'Missing required field: username' });
|
||||
return;
|
||||
}
|
||||
const teacher = await getTeacher(username);
|
||||
|
||||
const user = await getTeacher(username);
|
||||
|
||||
if (!user) {
|
||||
res.status(404).json({
|
||||
error: `Teacher '${username}' not found.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(user);
|
||||
res.json({ teacher });
|
||||
}
|
||||
|
||||
export async function createTeacherHandler(req: Request, res: Response) {
|
||||
export async function createTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.body.username;
|
||||
const firstName = req.body.firstName;
|
||||
const lastName = req.body.lastName;
|
||||
requireFields({ username, firstName, lastName });
|
||||
|
||||
const userData = req.body as TeacherDTO;
|
||||
|
||||
if (!userData.username || !userData.firstName || !userData.lastName) {
|
||||
res.status(400).json({
|
||||
error: 'Missing required fields: username, firstName, lastName',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newUser = await createTeacher(userData);
|
||||
|
||||
if (!newUser) {
|
||||
res.status(400).json({ error: 'Failed to create teacher' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(201).json(newUser);
|
||||
const teacher = await createTeacher(userData);
|
||||
res.json({ teacher });
|
||||
}
|
||||
|
||||
export async function deleteTeacherHandler(req: Request, res: Response) {
|
||||
export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
requireFields({ username });
|
||||
|
||||
if (!username) {
|
||||
res.status(400).json({ error: 'Missing required field: username' });
|
||||
return;
|
||||
}
|
||||
|
||||
const deletedUser = await deleteTeacher(username);
|
||||
if (!deletedUser) {
|
||||
res.status(404).json({
|
||||
error: `User '${username}' not found.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json(deletedUser);
|
||||
const teacher = await deleteTeacher(username);
|
||||
res.json({ teacher });
|
||||
}
|
||||
|
||||
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username as string;
|
||||
const username = req.params.username;
|
||||
const full = req.query.full === 'true';
|
||||
|
||||
if (!username) {
|
||||
res.status(400).json({ error: 'Missing required field: username' });
|
||||
return;
|
||||
}
|
||||
requireFields({ username });
|
||||
|
||||
const classes = await getClassesByTeacher(username, full);
|
||||
|
||||
if (!classes) {
|
||||
res.status(404).json({ error: 'Teacher not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ classes: classes });
|
||||
res.json({ classes });
|
||||
}
|
||||
|
||||
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username as string;
|
||||
const username = req.params.username;
|
||||
const full = req.query.full === 'true';
|
||||
|
||||
if (!username) {
|
||||
res.status(400).json({ error: 'Missing required field: username' });
|
||||
return;
|
||||
}
|
||||
requireFields({ username });
|
||||
|
||||
const students = await getStudentsByTeacher(username, full);
|
||||
|
||||
if (!students) {
|
||||
res.status(404).json({ error: 'Teacher not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ students: students });
|
||||
res.json({ students });
|
||||
}
|
||||
|
||||
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username as string;
|
||||
const username = req.params.username;
|
||||
const full = req.query.full === 'true';
|
||||
requireFields({ username });
|
||||
|
||||
if (!username) {
|
||||
res.status(400).json({ error: 'Missing required field: username' });
|
||||
return;
|
||||
}
|
||||
const questions = await getTeacherQuestions(username, full);
|
||||
|
||||
const questions = await getQuestionsByTeacher(username, full);
|
||||
|
||||
if (!questions) {
|
||||
res.status(404).json({ error: 'Teacher not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ questions: questions });
|
||||
res.json({ questions });
|
||||
}
|
||||
|
||||
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.classId;
|
||||
requireFields({ classId });
|
||||
|
||||
const joinRequests = await getJoinRequestsByClass(classId);
|
||||
res.json({ joinRequests });
|
||||
}
|
||||
|
||||
export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
||||
const studentUsername = req.params.studentUsername;
|
||||
const classId = req.params.classId;
|
||||
const accepted = req.body.accepted !== 'false'; // Default = true
|
||||
requireFields({ studentUsername, classId });
|
||||
|
||||
const request = await updateClassJoinRequestStatus(studentUsername, classId, accepted);
|
||||
res.json({ request });
|
||||
}
|
||||
|
|
|
@ -3,25 +3,23 @@ import { themes } from '../data/themes.js';
|
|||
import { loadTranslations } from '../util/translation-helper.js';
|
||||
|
||||
interface Translations {
|
||||
curricula_page: {
|
||||
[key: string]: { title: string; description?: string };
|
||||
};
|
||||
curricula_page: Record<string, { title: string; description?: string }>;
|
||||
}
|
||||
|
||||
export function getThemesHandler(req: Request, res: Response) {
|
||||
const language = (req.query.language as string)?.toLowerCase() || 'nl';
|
||||
export function getThemesHandler(req: Request, res: Response): void {
|
||||
const language = ((req.query.language as string) || 'nl').toLowerCase();
|
||||
const translations = loadTranslations<Translations>(language);
|
||||
const themeList = themes.map((theme) => ({
|
||||
key: theme.title,
|
||||
title: translations.curricula_page[theme.title]?.title || theme.title,
|
||||
description: translations.curricula_page[theme.title]?.description,
|
||||
title: translations.curricula_page[theme.title].title || theme.title,
|
||||
description: translations.curricula_page[theme.title].description,
|
||||
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
|
||||
}));
|
||||
|
||||
res.json(themeList);
|
||||
}
|
||||
|
||||
export function getHruidsByThemeHandler(req: Request, res: Response) {
|
||||
export function getHruidsByThemeHandler(req: Request, res: Response): void {
|
||||
const themeKey = req.params.theme;
|
||||
|
||||
if (!themeKey) {
|
||||
|
|
|
@ -3,13 +3,29 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
|||
import { Class } from '../../entities/classes/class.entity.js';
|
||||
|
||||
export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
|
||||
public findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
|
||||
return this.findOne({ within: within, id: id });
|
||||
public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
|
||||
return this.findOne({ within: within, id: id }, { populate: ['groups', 'groups.members'] });
|
||||
}
|
||||
public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
|
||||
return this.findAll({ where: { within: within } });
|
||||
public async findByClassIdAndAssignmentId(withinClass: string, id: number): Promise<Assignment | null> {
|
||||
return this.findOne({ within: { classId: withinClass }, id: id });
|
||||
}
|
||||
public deleteByClassAndId(within: Class, id: number): Promise<void> {
|
||||
public async findAllByResponsibleTeacher(teacherUsername: string): Promise<Assignment[]> {
|
||||
return this.findAll({
|
||||
where: {
|
||||
within: {
|
||||
teachers: {
|
||||
$some: {
|
||||
username: teacherUsername,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
|
||||
return this.findAll({ where: { within: within }, populate: ['groups', 'groups.members'] });
|
||||
}
|
||||
public async deleteByClassAndId(within: Class, id: number): Promise<void> {
|
||||
return this.deleteWhere({ within: within, id: id });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
|||
import { Student } from '../../entities/users/student.entity.js';
|
||||
|
||||
export class GroupRepository extends DwengoEntityRepository<Group> {
|
||||
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
|
||||
public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
|
||||
return this.findOne(
|
||||
{
|
||||
assignment: assignment,
|
||||
|
@ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository<Group> {
|
|||
{ populate: ['members'] }
|
||||
);
|
||||
}
|
||||
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
|
||||
public async findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
|
||||
return this.findAll({
|
||||
where: { assignment: assignment },
|
||||
populate: ['members'],
|
||||
});
|
||||
}
|
||||
public findAllGroupsWithStudent(student: Student): Promise<Group[]> {
|
||||
public async findAllGroupsWithStudent(student: Student): Promise<Group[]> {
|
||||
return this.find({ members: student }, { populate: ['members'] });
|
||||
}
|
||||
public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) {
|
||||
public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<void> {
|
||||
return this.deleteWhere({
|
||||
assignment: assignment,
|
||||
groupNumber: groupNumber,
|
||||
|
|
|
@ -3,29 +3,30 @@ import { Group } from '../../entities/assignments/group.entity.js';
|
|||
import { Submission } from '../../entities/assignments/submission.entity.js';
|
||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||
import { Student } from '../../entities/users/student.entity.js';
|
||||
import { Assignment } from '../../entities/assignments/assignment.entity';
|
||||
|
||||
export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
||||
public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission | null> {
|
||||
return this.findOne(
|
||||
{
|
||||
learningObjectHruid: loId.hruid,
|
||||
learningObjectLanguage: loId.language,
|
||||
learningObjectVersion: loId.version,
|
||||
submissionNumber: submissionNumber,
|
||||
},
|
||||
{ populate: ['submitter', 'onBehalfOf'] },
|
||||
);
|
||||
public async findSubmissionByLearningObjectAndSubmissionNumber(
|
||||
loId: LearningObjectIdentifier,
|
||||
submissionNumber: number
|
||||
): Promise<Submission | null> {
|
||||
return this.findOne({
|
||||
learningObjectHruid: loId.hruid,
|
||||
learningObjectLanguage: loId.language,
|
||||
learningObjectVersion: loId.version,
|
||||
submissionNumber: submissionNumber,
|
||||
});
|
||||
}
|
||||
|
||||
public findSubmissionsByLearningObject(loId: LearningObjectIdentifier): Promise<Submission[]> {
|
||||
public async findByLearningObject(loId: LearningObjectIdentifier): Promise<Submission[]> {
|
||||
return this.find({
|
||||
learningObjectHruid: loId.hruid,
|
||||
learningObjectLanguage: loId.language,
|
||||
learningObjectVersion: loId.version,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
|
||||
public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
|
||||
return this.findOne(
|
||||
{
|
||||
learningObjectHruid: loId.hruid,
|
||||
|
@ -37,7 +38,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
|||
);
|
||||
}
|
||||
|
||||
public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> {
|
||||
public async findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> {
|
||||
return this.findOne(
|
||||
{
|
||||
learningObjectHruid: loId.hruid,
|
||||
|
@ -49,15 +50,60 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
|||
);
|
||||
}
|
||||
|
||||
public findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
|
||||
return this.find({ onBehalfOf: group });
|
||||
public async findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
|
||||
return this.find(
|
||||
{ onBehalfOf: group },
|
||||
{
|
||||
populate: ['onBehalfOf.members'],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
|
||||
return this.find({ submitter: student });
|
||||
/**
|
||||
* Looks up all submissions for the given learning object which were submitted as part of the given assignment.
|
||||
*/
|
||||
public async findAllSubmissionsForLearningObjectAndAssignment(loId: LearningObjectIdentifier, assignment: Assignment): Promise<Submission[]> {
|
||||
return this.findAll({
|
||||
where: {
|
||||
learningObjectHruid: loId.hruid,
|
||||
learningObjectLanguage: loId.language,
|
||||
learningObjectVersion: loId.version,
|
||||
onBehalfOf: {
|
||||
assignment,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
|
||||
/**
|
||||
* Looks up all submissions for the given learning object which were submitted by the given group
|
||||
*/
|
||||
public async findAllSubmissionsForLearningObjectAndGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission[]> {
|
||||
return this.findAll({
|
||||
where: {
|
||||
learningObjectHruid: loId.hruid,
|
||||
learningObjectLanguage: loId.language,
|
||||
learningObjectVersion: loId.version,
|
||||
onBehalfOf: group,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
|
||||
const result = await this.find(
|
||||
{ submitter: student },
|
||||
{
|
||||
populate: ['onBehalfOf.members'],
|
||||
}
|
||||
);
|
||||
|
||||
// Workaround: For some reason, without this MikroORM generates an UPDATE query with a syntax error in some tests
|
||||
this.em.clear();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
|
||||
return this.deleteWhere({
|
||||
learningObjectHruid: loId.hruid,
|
||||
learningObjectLanguage: loId.language,
|
||||
|
|
|
@ -2,15 +2,19 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
|||
import { Class } from '../../entities/classes/class.entity.js';
|
||||
import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js';
|
||||
import { Student } from '../../entities/users/student.entity.js';
|
||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||
|
||||
export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> {
|
||||
public findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
|
||||
public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
|
||||
return this.findAll({ where: { requester: requester } });
|
||||
}
|
||||
public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
|
||||
return this.findAll({ where: { class: clazz } });
|
||||
public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
|
||||
return this.findAll({ where: { class: clazz, status: ClassStatus.Open } }); // TODO check if works like this
|
||||
}
|
||||
public deleteBy(requester: Student, clazz: Class): Promise<void> {
|
||||
public async findByStudentAndClass(requester: Student, clazz: Class): Promise<ClassJoinRequest | null> {
|
||||
return this.findOne({ requester, class: clazz });
|
||||
}
|
||||
public async deleteBy(requester: Student, clazz: Class): Promise<void> {
|
||||
return this.deleteWhere({ requester: requester, class: clazz });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js';
|
|||
import { Teacher } from '../../entities/users/teacher.entity';
|
||||
|
||||
export class ClassRepository extends DwengoEntityRepository<Class> {
|
||||
public findById(id: string): Promise<Class | null> {
|
||||
public async findById(id: string): Promise<Class | null> {
|
||||
return this.findOne({ classId: id }, { populate: ['students', 'teachers'] });
|
||||
}
|
||||
public deleteById(id: string): Promise<void> {
|
||||
public async deleteById(id: string): Promise<void> {
|
||||
return this.deleteWhere({ classId: id });
|
||||
}
|
||||
public findByStudent(student: Student): Promise<Class[]> {
|
||||
public async findByStudent(student: Student): Promise<Class[]> {
|
||||
return this.find(
|
||||
{ students: student },
|
||||
{ populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
|
||||
);
|
||||
}
|
||||
|
||||
public findByTeacher(teacher: Teacher): Promise<Class[]> {
|
||||
public async findByTeacher(teacher: Teacher): Promise<Class[]> {
|
||||
return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,22 +2,30 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
|||
import { Class } from '../../entities/classes/class.entity.js';
|
||||
import { TeacherInvitation } from '../../entities/classes/teacher-invitation.entity.js';
|
||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||
|
||||
export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> {
|
||||
public findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> {
|
||||
public async findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> {
|
||||
return this.findAll({ where: { class: clazz } });
|
||||
}
|
||||
public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
|
||||
public async findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
|
||||
return this.findAll({ where: { sender: sender } });
|
||||
}
|
||||
public findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> {
|
||||
return this.findAll({ where: { receiver: receiver } });
|
||||
public async findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> {
|
||||
return this.findAll({ where: { receiver: receiver, status: ClassStatus.Open } });
|
||||
}
|
||||
public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
|
||||
public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
|
||||
return this.deleteWhere({
|
||||
sender: sender,
|
||||
receiver: receiver,
|
||||
class: clazz,
|
||||
});
|
||||
}
|
||||
public async findBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<TeacherInvitation | null> {
|
||||
return this.findOne({
|
||||
sender: sender,
|
||||
receiver: receiver,
|
||||
class: clazz,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||
import { Attachment } from '../../entities/content/attachment.entity.js';
|
||||
import { Language } from '../../entities/content/language';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier';
|
||||
|
||||
export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
|
||||
public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> {
|
||||
public async findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> {
|
||||
return this.findOne({
|
||||
learningObject: {
|
||||
hruid: learningObjectId.hruid,
|
||||
|
@ -15,7 +15,11 @@ export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
|
|||
});
|
||||
}
|
||||
|
||||
public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise<Attachment | null> {
|
||||
public async findByMostRecentVersionOfLearningObjectAndName(
|
||||
hruid: string,
|
||||
language: Language,
|
||||
attachmentName: string
|
||||
): Promise<Attachment | null> {
|
||||
return this.findOne(
|
||||
{
|
||||
learningObject: {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||
import { Language } from '../../entities/content/language.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||
|
||||
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
||||
public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||
return this.findOne(
|
||||
{
|
||||
hruid: identifier.hruid,
|
||||
|
@ -18,7 +18,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
|||
);
|
||||
}
|
||||
|
||||
public findLatestByHruidAndLanguage(hruid: string, language: Language) {
|
||||
public async findLatestByHruidAndLanguage(hruid: string, language: Language): Promise<LearningObject | null> {
|
||||
return this.findOne(
|
||||
{
|
||||
hruid: hruid,
|
||||
|
@ -33,7 +33,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
|||
);
|
||||
}
|
||||
|
||||
public findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
||||
public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
||||
return this.find(
|
||||
{ admins: teacher },
|
||||
{ populate: ['admins'] } // Make sure to load admin relations
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||
import { LearningPath } from '../../entities/content/learning-path.entity.js';
|
||||
import { Language } from '../../entities/content/language.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
|
||||
import { RequiredEntityData } from '@mikro-orm/core';
|
||||
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||
import { EntityAlreadyExistsException } from '../../exceptions/entity-already-exists-exception.js';
|
||||
|
||||
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
|
||||
public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
|
||||
public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
|
||||
return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] });
|
||||
}
|
||||
|
||||
|
@ -23,4 +27,27 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
|
|||
populate: ['nodes', 'nodes.transitions'],
|
||||
});
|
||||
}
|
||||
|
||||
public createNode(nodeData: RequiredEntityData<LearningPathNode>): LearningPathNode {
|
||||
return this.em.create(LearningPathNode, nodeData);
|
||||
}
|
||||
|
||||
public createTransition(transitionData: RequiredEntityData<LearningPathTransition>): LearningPathTransition {
|
||||
return this.em.create(LearningPathTransition, transitionData);
|
||||
}
|
||||
|
||||
public async saveLearningPathNodesAndTransitions(
|
||||
path: LearningPath,
|
||||
nodes: LearningPathNode[],
|
||||
transitions: LearningPathTransition[],
|
||||
options?: { preventOverwrite?: boolean }
|
||||
): Promise<void> {
|
||||
if (options?.preventOverwrite && (await this.findOne(path))) {
|
||||
throw new EntityAlreadyExistsException('A learning path with this hruid/language combination already exists.');
|
||||
}
|
||||
const em = this.getEntityManager();
|
||||
await em.persistAndFlush(path);
|
||||
await Promise.all(nodes.map(async (it) => em.persistAndFlush(it)));
|
||||
await Promise.all(transitions.map(async (it) => em.persistAndFlush(it)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
|
||||
import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js';
|
||||
|
||||
export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
|
||||
public async save(entity: T) {
|
||||
const em = this.getEntityManager();
|
||||
em.persist(entity);
|
||||
await em.flush();
|
||||
public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> {
|
||||
if (options?.preventOverwrite && (await this.findOne(entity))) {
|
||||
throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`);
|
||||
}
|
||||
await this.getEntityManager().persistAndFlush(entity);
|
||||
}
|
||||
public async deleteWhere(query: FilterQuery<T>) {
|
||||
public async deleteWhere(query: FilterQuery<T>): Promise<void> {
|
||||
const toDelete = await this.findOne(query);
|
||||
const em = this.getEntityManager();
|
||||
if (toDelete) {
|
||||
|
|
|
@ -2,27 +2,43 @@ 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 createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
|
||||
public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
|
||||
const answerEntity = this.create({
|
||||
toQuestion: answer.toQuestion,
|
||||
author: answer.author,
|
||||
content: answer.content,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
return this.insert(answerEntity);
|
||||
await this.insert(answerEntity);
|
||||
answerEntity.toQuestion = answer.toQuestion;
|
||||
answerEntity.author = answer.author;
|
||||
answerEntity.content = answer.content;
|
||||
return answerEntity;
|
||||
}
|
||||
public findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
|
||||
public async findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
|
||||
return this.findAll({
|
||||
where: { toQuestion: question },
|
||||
orderBy: { sequenceNumber: 'ASC' },
|
||||
});
|
||||
}
|
||||
public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,18 @@ import { Question } from '../../entities/questions/question.entity.js';
|
|||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||
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';
|
||||
import { Group } from '../../entities/assignments/group.entity';
|
||||
|
||||
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
|
||||
public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> {
|
||||
const questionEntity = this.create({
|
||||
learningObjectHruid: question.loId.hruid,
|
||||
learningObjectLanguage: question.loId.language,
|
||||
learningObjectVersion: question.loId.version,
|
||||
author: question.author,
|
||||
inGroup: question.inGroup,
|
||||
content: question.content,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
@ -18,10 +22,11 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
|||
questionEntity.learningObjectLanguage = question.loId.language;
|
||||
questionEntity.learningObjectVersion = question.loId.version;
|
||||
questionEntity.author = question.author;
|
||||
questionEntity.inGroup = question.inGroup;
|
||||
questionEntity.content = question.content;
|
||||
return this.insert(questionEntity);
|
||||
return await this.insert(questionEntity);
|
||||
}
|
||||
public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
|
||||
public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
|
||||
return this.findAll({
|
||||
where: {
|
||||
learningObjectHruid: loId.hruid,
|
||||
|
@ -33,7 +38,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
|||
},
|
||||
});
|
||||
}
|
||||
public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
|
||||
public async removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
|
||||
return this.deleteWhere({
|
||||
learningObjectHruid: loId.hruid,
|
||||
learningObjectLanguage: loId.language,
|
||||
|
@ -54,4 +59,73 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
|||
orderBy: { timestamp: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
public async findAllByAssignment(assignment: Assignment): Promise<Question[]> {
|
||||
return this.find({
|
||||
inGroup: assignment.groups.getItems(),
|
||||
learningObjectHruid: assignment.learningPathHruid,
|
||||
learningObjectLanguage: assignment.learningPathLanguage,
|
||||
});
|
||||
}
|
||||
|
||||
public async findAllByAuthor(author: Student): Promise<Question[]> {
|
||||
return this.findAll({
|
||||
where: { author },
|
||||
orderBy: { timestamp: 'DESC' }, // New to old
|
||||
});
|
||||
}
|
||||
|
||||
public async findAllByGroup(inGroup: Group): Promise<Question[]> {
|
||||
return this.findAll({
|
||||
where: { inGroup },
|
||||
orderBy: { timestamp: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up all questions for the given learning object which were asked as part of the given assignment.
|
||||
* When forStudentUsername is set, only the questions within the given user's group are shown.
|
||||
*/
|
||||
public async findAllQuestionsAboutLearningObjectInAssignment(
|
||||
loId: LearningObjectIdentifier,
|
||||
assignment: Assignment,
|
||||
forStudentUsername?: string
|
||||
): Promise<Question[]> {
|
||||
const inGroup = forStudentUsername
|
||||
? {
|
||||
assignment,
|
||||
members: {
|
||||
$some: {
|
||||
username: forStudentUsername,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
assignment,
|
||||
};
|
||||
|
||||
return this.findAll({
|
||||
where: {
|
||||
learningObjectHruid: loId.hruid,
|
||||
learningObjectLanguage: loId.language,
|
||||
learningObjectVersion: loId.version,
|
||||
inGroup,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,8 +34,8 @@ let entityManager: EntityManager | undefined;
|
|||
/**
|
||||
* Execute all the database operations within the function f in a single transaction.
|
||||
*/
|
||||
export function transactional<T>(f: () => Promise<T>) {
|
||||
entityManager?.transactional(f);
|
||||
export async function transactional<T>(f: () => Promise<T>): Promise<void> {
|
||||
await entityManager?.transactional(f);
|
||||
}
|
||||
|
||||
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R {
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
export interface Theme {
|
||||
title: string;
|
||||
hruids: string[];
|
||||
}
|
||||
import { Theme } from '@dwengo-1/common/interfaces/theme';
|
||||
|
||||
export const themes: Theme[] = [
|
||||
{
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
import { Class } from '../../entities/classes/class.entity.js';
|
||||
import { Student } from '../../entities/users/student.entity.js';
|
||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||
// Import { UserRepository } from './user-repository.js';
|
||||
|
||||
// Export class StudentRepository extends UserRepository<Student> {}
|
||||
|
||||
export class StudentRepository extends DwengoEntityRepository<Student> {
|
||||
public findByUsername(username: string): Promise<Student | null> {
|
||||
public async findByUsername(username: string): Promise<Student | null> {
|
||||
return this.findOne({ username: username });
|
||||
}
|
||||
public findByClass(cls: Class): Promise<Student[]> {
|
||||
return this.find({ classes: cls });
|
||||
}
|
||||
public deleteByUsername(username: string): Promise<void> {
|
||||
public async deleteByUsername(username: string): Promise<void> {
|
||||
return this.deleteWhere({ username: username });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ import { Teacher } from '../../entities/users/teacher.entity.js';
|
|||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||
|
||||
export class TeacherRepository extends DwengoEntityRepository<Teacher> {
|
||||
public findByUsername(username: string): Promise<Teacher | null> {
|
||||
public async findByUsername(username: string): Promise<Teacher | null> {
|
||||
return this.findOne({ username: username });
|
||||
}
|
||||
public deleteByUsername(username: string): Promise<void> {
|
||||
public async deleteByUsername(username: string): Promise<void> {
|
||||
return this.deleteWhere({ username: username });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
|||
import { User } from '../../entities/users/user.entity.js';
|
||||
|
||||
export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
|
||||
public findByUsername(username: string): Promise<T | null> {
|
||||
public async findByUsername(username: string): Promise<T | null> {
|
||||
return this.findOne({ username } as Partial<T>);
|
||||
}
|
||||
public deleteByUsername(username: string): Promise<void> {
|
||||
public async deleteByUsername(username: string): Promise<void> {
|
||||
return this.deleteWhere({ username } as Partial<T>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Cascade, Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Class } from '../classes/class.entity.js';
|
||||
import { Group } from './group.entity.js';
|
||||
import { Language } from '../content/language.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { AssignmentRepository } from '../../data/assignments/assignment-repository.js';
|
||||
|
||||
@Entity({
|
||||
|
@ -14,7 +14,7 @@ export class Assignment {
|
|||
})
|
||||
within!: Class;
|
||||
|
||||
@PrimaryKey({ type: 'number', autoincrement: true })
|
||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||
id?: number;
|
||||
|
||||
@Property({ type: 'string' })
|
||||
|
@ -34,6 +34,7 @@ export class Assignment {
|
|||
@OneToMany({
|
||||
entity: () => Group,
|
||||
mappedBy: 'assignment',
|
||||
cascade: [Cascade.ALL],
|
||||
})
|
||||
groups!: Group[];
|
||||
groups: Collection<Group> = new Collection<Group>(this);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
|
||||
import { Collection, Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
|
||||
import { Assignment } from './assignment.entity.js';
|
||||
import { Student } from '../users/student.entity.js';
|
||||
import { GroupRepository } from '../../data/assignments/group-repository.js';
|
||||
|
@ -7,17 +7,23 @@ import { GroupRepository } from '../../data/assignments/group-repository.js';
|
|||
repository: () => GroupRepository,
|
||||
})
|
||||
export class Group {
|
||||
/*
|
||||
WARNING: Don't move the definition of groupNumber! If it does not come before the definition of assignment,
|
||||
creating groups fails because of a MikroORM bug!
|
||||
*/
|
||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||
groupNumber?: number;
|
||||
|
||||
@ManyToOne({
|
||||
entity: () => Assignment,
|
||||
primary: true,
|
||||
})
|
||||
assignment!: Assignment;
|
||||
|
||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||
groupNumber?: number;
|
||||
|
||||
@ManyToMany({
|
||||
entity: () => Student,
|
||||
owner: true,
|
||||
inversedBy: 'groups',
|
||||
})
|
||||
members!: Student[];
|
||||
members: Collection<Student> = new Collection<Student>(this);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { Student } from '../users/student.entity.js';
|
||||
import { Group } from './group.entity.js';
|
||||
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Language } from '../content/language.js';
|
||||
import { Entity, Enum, ManyToOne, PrimaryKey, Property, Cascade } from '@mikro-orm/core';
|
||||
import { SubmissionRepository } from '../../data/assignments/submission-repository.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
||||
@Entity({ repository: () => SubmissionRepository })
|
||||
export class Submission {
|
||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||
submissionNumber?: number;
|
||||
|
||||
@PrimaryKey({ type: 'string' })
|
||||
learningObjectHruid!: string;
|
||||
|
||||
|
@ -15,11 +18,13 @@ export class Submission {
|
|||
})
|
||||
learningObjectLanguage!: Language;
|
||||
|
||||
@PrimaryKey({ type: 'numeric' })
|
||||
learningObjectVersion: number = 1;
|
||||
@PrimaryKey({ type: 'numeric', autoincrement: false })
|
||||
learningObjectVersion = 1;
|
||||
|
||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||
submissionNumber?: number;
|
||||
@ManyToOne(() => Group, {
|
||||
cascade: [Cascade.REMOVE],
|
||||
})
|
||||
onBehalfOf!: Group;
|
||||
|
||||
@ManyToOne({
|
||||
entity: () => Student,
|
||||
|
@ -29,12 +34,6 @@ export class Submission {
|
|||
@Property({ type: 'datetime' })
|
||||
submissionTime!: Date;
|
||||
|
||||
@ManyToOne({
|
||||
entity: () => Group,
|
||||
nullable: true,
|
||||
})
|
||||
onBehalfOf?: Group;
|
||||
|
||||
@Property({ type: 'json' })
|
||||
content!: string;
|
||||
}
|
||||
|
|
|
@ -2,12 +2,7 @@ import { Entity, Enum, ManyToOne } from '@mikro-orm/core';
|
|||
import { Student } from '../users/student.entity.js';
|
||||
import { Class } from './class.entity.js';
|
||||
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
|
||||
|
||||
export enum ClassJoinRequestStatus {
|
||||
Open = 'open',
|
||||
Accepted = 'accepted',
|
||||
Declined = 'declined',
|
||||
}
|
||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||
|
||||
@Entity({
|
||||
repository: () => ClassJoinRequestRepository,
|
||||
|
@ -25,6 +20,6 @@ export class ClassJoinRequest {
|
|||
})
|
||||
class!: Class;
|
||||
|
||||
@Enum(() => ClassJoinRequestStatus)
|
||||
status!: ClassJoinRequestStatus;
|
||||
}
|
||||
@Enum(() => ClassStatus)
|
||||
status!: ClassStatus;
|
||||
}
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { v4 } from 'uuid';
|
||||
import { Teacher } from '../users/teacher.entity.js';
|
||||
import { Student } from '../users/student.entity.js';
|
||||
import { ClassRepository } from '../../data/classes/class-repository.js';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
const generateClassId = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 6);
|
||||
|
||||
@Entity({
|
||||
repository: () => ClassRepository,
|
||||
})
|
||||
export class Class {
|
||||
@PrimaryKey()
|
||||
classId? = v4();
|
||||
classId? = generateClassId();
|
||||
|
||||
@Property({ type: 'string' })
|
||||
displayName!: string;
|
||||
|
||||
@ManyToMany(() => Teacher)
|
||||
@ManyToMany({ entity: () => Teacher, owner: true, inversedBy: 'classes' })
|
||||
teachers!: Collection<Teacher>;
|
||||
|
||||
@ManyToMany(() => Student)
|
||||
@ManyToMany({ entity: () => Student, owner: true, inversedBy: 'classes' })
|
||||
students!: Collection<Student>;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { Entity, ManyToOne } from '@mikro-orm/core';
|
||||
import { Entity, Enum, ManyToOne } from '@mikro-orm/core';
|
||||
import { Teacher } from '../users/teacher.entity.js';
|
||||
import { Class } from './class.entity.js';
|
||||
import { TeacherInvitationRepository } from '../../data/classes/teacher-invitation-repository.js';
|
||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||
|
||||
/**
|
||||
* Invitation of a teacher into a class (in order to teach it).
|
||||
|
@ -25,4 +26,7 @@ export class TeacherInvitation {
|
|||
primary: true,
|
||||
})
|
||||
class!: Class;
|
||||
|
||||
@Enum(() => ClassStatus)
|
||||
status!: ClassStatus;
|
||||
}
|
||||
|
|
10
backend/src/entities/content/educational-goal.entity.ts
Normal file
10
backend/src/entities/content/educational-goal.entity.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Embeddable, Property } from '@mikro-orm/core';
|
||||
|
||||
@Embeddable()
|
||||
export class EducationalGoal {
|
||||
@Property({ type: 'string' })
|
||||
source!: string;
|
||||
|
||||
@Property({ type: 'string' })
|
||||
id!: string;
|
||||
}
|
|
@ -1,193 +0,0 @@
|
|||
export enum Language {
|
||||
Afar = 'aa',
|
||||
Abkhazian = 'ab',
|
||||
Afrikaans = 'af',
|
||||
Akan = 'ak',
|
||||
Albanian = 'sq',
|
||||
Amharic = 'am',
|
||||
Arabic = 'ar',
|
||||
Aragonese = 'an',
|
||||
Armenian = 'hy',
|
||||
Assamese = 'as',
|
||||
Avaric = 'av',
|
||||
Avestan = 'ae',
|
||||
Aymara = 'ay',
|
||||
Azerbaijani = 'az',
|
||||
Bashkir = 'ba',
|
||||
Bambara = 'bm',
|
||||
Basque = 'eu',
|
||||
Belarusian = 'be',
|
||||
Bengali = 'bn',
|
||||
Bihari = 'bh',
|
||||
Bislama = 'bi',
|
||||
Bosnian = 'bs',
|
||||
Breton = 'br',
|
||||
Bulgarian = 'bg',
|
||||
Burmese = 'my',
|
||||
Catalan = 'ca',
|
||||
Chamorro = 'ch',
|
||||
Chechen = 'ce',
|
||||
Chinese = 'zh',
|
||||
ChurchSlavic = 'cu',
|
||||
Chuvash = 'cv',
|
||||
Cornish = 'kw',
|
||||
Corsican = 'co',
|
||||
Cree = 'cr',
|
||||
Czech = 'cs',
|
||||
Danish = 'da',
|
||||
Divehi = 'dv',
|
||||
Dutch = 'nl',
|
||||
Dzongkha = 'dz',
|
||||
English = 'en',
|
||||
Esperanto = 'eo',
|
||||
Estonian = 'et',
|
||||
Ewe = 'ee',
|
||||
Faroese = 'fo',
|
||||
Fijian = 'fj',
|
||||
Finnish = 'fi',
|
||||
French = 'fr',
|
||||
Frisian = 'fy',
|
||||
Fulah = 'ff',
|
||||
Georgian = 'ka',
|
||||
German = 'de',
|
||||
Gaelic = 'gd',
|
||||
Irish = 'ga',
|
||||
Galician = 'gl',
|
||||
Manx = 'gv',
|
||||
Greek = 'el',
|
||||
Guarani = 'gn',
|
||||
Gujarati = 'gu',
|
||||
Haitian = 'ht',
|
||||
Hausa = 'ha',
|
||||
Hebrew = 'he',
|
||||
Herero = 'hz',
|
||||
Hindi = 'hi',
|
||||
HiriMotu = 'ho',
|
||||
Croatian = 'hr',
|
||||
Hungarian = 'hu',
|
||||
Igbo = 'ig',
|
||||
Icelandic = 'is',
|
||||
Ido = 'io',
|
||||
SichuanYi = 'ii',
|
||||
Inuktitut = 'iu',
|
||||
Interlingue = 'ie',
|
||||
Interlingua = 'ia',
|
||||
Indonesian = 'id',
|
||||
Inupiaq = 'ik',
|
||||
Italian = 'it',
|
||||
Javanese = 'jv',
|
||||
Japanese = 'ja',
|
||||
Kalaallisut = 'kl',
|
||||
Kannada = 'kn',
|
||||
Kashmiri = 'ks',
|
||||
Kanuri = 'kr',
|
||||
Kazakh = 'kk',
|
||||
Khmer = 'km',
|
||||
Kikuyu = 'ki',
|
||||
Kinyarwanda = 'rw',
|
||||
Kirghiz = 'ky',
|
||||
Komi = 'kv',
|
||||
Kongo = 'kg',
|
||||
Korean = 'ko',
|
||||
Kuanyama = 'kj',
|
||||
Kurdish = 'ku',
|
||||
Lao = 'lo',
|
||||
Latin = 'la',
|
||||
Latvian = 'lv',
|
||||
Limburgan = 'li',
|
||||
Lingala = 'ln',
|
||||
Lithuanian = 'lt',
|
||||
Luxembourgish = 'lb',
|
||||
LubaKatanga = 'lu',
|
||||
Ganda = 'lg',
|
||||
Macedonian = 'mk',
|
||||
Marshallese = 'mh',
|
||||
Malayalam = 'ml',
|
||||
Maori = 'mi',
|
||||
Marathi = 'mr',
|
||||
Malay = 'ms',
|
||||
Malagasy = 'mg',
|
||||
Maltese = 'mt',
|
||||
Mongolian = 'mn',
|
||||
Nauru = 'na',
|
||||
Navajo = 'nv',
|
||||
SouthNdebele = 'nr',
|
||||
NorthNdebele = 'nd',
|
||||
Ndonga = 'ng',
|
||||
Nepali = 'ne',
|
||||
NorwegianNynorsk = 'nn',
|
||||
NorwegianBokmal = 'nb',
|
||||
Norwegian = 'no',
|
||||
Chichewa = 'ny',
|
||||
Occitan = 'oc',
|
||||
Ojibwa = 'oj',
|
||||
Oriya = 'or',
|
||||
Oromo = 'om',
|
||||
Ossetian = 'os',
|
||||
Punjabi = 'pa',
|
||||
Persian = 'fa',
|
||||
Pali = 'pi',
|
||||
Polish = 'pl',
|
||||
Portuguese = 'pt',
|
||||
Pashto = 'ps',
|
||||
Quechua = 'qu',
|
||||
Romansh = 'rm',
|
||||
Romanian = 'ro',
|
||||
Rundi = 'rn',
|
||||
Russian = 'ru',
|
||||
Sango = 'sg',
|
||||
Sanskrit = 'sa',
|
||||
Sinhala = 'si',
|
||||
Slovak = 'sk',
|
||||
Slovenian = 'sl',
|
||||
NorthernSami = 'se',
|
||||
Samoan = 'sm',
|
||||
Shona = 'sn',
|
||||
Sindhi = 'sd',
|
||||
Somali = 'so',
|
||||
Sotho = 'st',
|
||||
Spanish = 'es',
|
||||
Sardinian = 'sc',
|
||||
Serbian = 'sr',
|
||||
Swati = 'ss',
|
||||
Sundanese = 'su',
|
||||
Swahili = 'sw',
|
||||
Swedish = 'sv',
|
||||
Tahitian = 'ty',
|
||||
Tamil = 'ta',
|
||||
Tatar = 'tt',
|
||||
Telugu = 'te',
|
||||
Tajik = 'tg',
|
||||
Tagalog = 'tl',
|
||||
Thai = 'th',
|
||||
Tibetan = 'bo',
|
||||
Tigrinya = 'ti',
|
||||
Tonga = 'to',
|
||||
Tswana = 'tn',
|
||||
Tsonga = 'ts',
|
||||
Turkmen = 'tk',
|
||||
Turkish = 'tr',
|
||||
Twi = 'tw',
|
||||
Uighur = 'ug',
|
||||
Ukrainian = 'uk',
|
||||
Urdu = 'ur',
|
||||
Uzbek = 'uz',
|
||||
Venda = 've',
|
||||
Vietnamese = 'vi',
|
||||
Volapuk = 'vo',
|
||||
Welsh = 'cy',
|
||||
Walloon = 'wa',
|
||||
Wolof = 'wo',
|
||||
Xhosa = 'xh',
|
||||
Yiddish = 'yi',
|
||||
Yoruba = 'yo',
|
||||
Zhuang = 'za',
|
||||
Zulu = 'zu',
|
||||
}
|
||||
|
||||
export const languageMap: Record<string, Language> = {
|
||||
nl: Language.Dutch,
|
||||
fr: Language.French,
|
||||
en: Language.English,
|
||||
de: Language.German,
|
||||
};
|
|
@ -1,9 +1,11 @@
|
|||
import { Language } from './language.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
||||
export class LearningObjectIdentifier {
|
||||
constructor(
|
||||
public hruid: string,
|
||||
public language: Language,
|
||||
public version: number
|
||||
) {}
|
||||
) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,12 @@
|
|||
import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Language } from './language.js';
|
||||
import { ArrayType, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Attachment } from './attachment.entity.js';
|
||||
import { Teacher } from '../users/teacher.entity.js';
|
||||
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
|
||||
import { v4 } from 'uuid';
|
||||
import { LearningObjectRepository } from '../../data/content/learning-object-repository.js';
|
||||
|
||||
@Embeddable()
|
||||
export class EducationalGoal {
|
||||
@Property({ type: 'string' })
|
||||
source!: string;
|
||||
|
||||
@Property({ type: 'string' })
|
||||
id!: string;
|
||||
}
|
||||
|
||||
@Embeddable()
|
||||
export class ReturnValue {
|
||||
@Property({ type: 'string' })
|
||||
callbackUrl!: string;
|
||||
|
||||
@Property({ type: 'json' })
|
||||
callbackSchema!: string;
|
||||
}
|
||||
import { EducationalGoal } from './educational-goal.entity.js';
|
||||
import { ReturnValue } from './return-value.entity.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
||||
@Entity({ repository: () => LearningObjectRepository })
|
||||
export class LearningObject {
|
||||
|
@ -36,7 +20,7 @@ export class LearningObject {
|
|||
language!: Language;
|
||||
|
||||
@PrimaryKey({ type: 'number' })
|
||||
version: number = 1;
|
||||
version = 1;
|
||||
|
||||
@Property({ type: 'uuid', unique: true })
|
||||
uuid = v4();
|
||||
|
@ -58,11 +42,11 @@ export class LearningObject {
|
|||
@Property({ type: 'array' })
|
||||
keywords: string[] = [];
|
||||
|
||||
@Property({ type: 'array', nullable: true })
|
||||
@Property({ type: new ArrayType((i) => Number(i)), nullable: true })
|
||||
targetAges?: number[] = [];
|
||||
|
||||
@Property({ type: 'bool' })
|
||||
teacherExclusive: boolean = false;
|
||||
teacherExclusive = false;
|
||||
|
||||
@Property({ type: 'array' })
|
||||
skosConcepts: string[] = [];
|
||||
|
@ -74,10 +58,10 @@ export class LearningObject {
|
|||
educationalGoals: EducationalGoal[] = [];
|
||||
|
||||
@Property({ type: 'string' })
|
||||
copyright: string = '';
|
||||
copyright = '';
|
||||
|
||||
@Property({ type: 'string' })
|
||||
license: string = '';
|
||||
license = '';
|
||||
|
||||
@Property({ type: 'smallint', nullable: true })
|
||||
difficulty?: number;
|
||||
|
@ -91,7 +75,7 @@ export class LearningObject {
|
|||
returnValue!: ReturnValue;
|
||||
|
||||
@Property({ type: 'bool' })
|
||||
available: boolean = true;
|
||||
available = true;
|
||||
|
||||
@Property({ type: 'string', nullable: true })
|
||||
contentLocation?: string;
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core';
|
||||
import { Language } from './language.js';
|
||||
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core';
|
||||
import { LearningPath } from './learning-path.entity.js';
|
||||
import { LearningPathTransition } from './learning-path-transition.entity.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
||||
@Entity()
|
||||
export class LearningPathNode {
|
||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||
nodeNumber?: number;
|
||||
|
||||
@ManyToOne({ entity: () => LearningPath, primary: true })
|
||||
learningPath!: Rel<LearningPath>;
|
||||
|
||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||
nodeNumber!: number;
|
||||
|
||||
@Property({ type: 'string' })
|
||||
learningObjectHruid!: string;
|
||||
|
||||
|
@ -27,7 +27,7 @@ export class LearningPathNode {
|
|||
startNode!: boolean;
|
||||
|
||||
@OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' })
|
||||
transitions: LearningPathTransition[] = [];
|
||||
transitions!: Collection<LearningPathTransition>;
|
||||
|
||||
@Property({ length: 3 })
|
||||
createdAt: Date = new Date();
|
||||
|
|
|
@ -3,12 +3,12 @@ import { LearningPathNode } from './learning-path-node.entity.js';
|
|||
|
||||
@Entity()
|
||||
export class LearningPathTransition {
|
||||
@ManyToOne({ entity: () => LearningPathNode, primary: true })
|
||||
node!: Rel<LearningPathNode>;
|
||||
|
||||
@PrimaryKey({ type: 'numeric' })
|
||||
transitionNumber!: number;
|
||||
|
||||
@ManyToOne({ entity: () => LearningPathNode, primary: true })
|
||||
node!: Rel<LearningPathNode>;
|
||||
|
||||
@Property({ type: 'string' })
|
||||
condition!: string;
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Language } from './language.js';
|
||||
import { Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Teacher } from '../users/teacher.entity.js';
|
||||
import { LearningPathRepository } from '../../data/content/learning-path-repository.js';
|
||||
import { LearningPathNode } from './learning-path-node.entity.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
||||
@Entity({ repository: () => LearningPathRepository })
|
||||
export class LearningPath {
|
||||
|
@ -25,5 +25,5 @@ export class LearningPath {
|
|||
image: Buffer | null = null;
|
||||
|
||||
@OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' })
|
||||
nodes: LearningPathNode[] = [];
|
||||
nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this);
|
||||
}
|
||||
|
|
10
backend/src/entities/content/return-value.entity.ts
Normal file
10
backend/src/entities/content/return-value.entity.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Embeddable, Property } from '@mikro-orm/core';
|
||||
|
||||
@Embeddable()
|
||||
export class ReturnValue {
|
||||
@Property({ type: 'string' })
|
||||
callbackUrl!: string;
|
||||
|
||||
@Property({ type: 'json' })
|
||||
callbackSchema!: string;
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Language } from '../content/language.js';
|
||||
import { Student } from '../users/student.entity.js';
|
||||
import { QuestionRepository } from '../../data/questions/question-repository.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { Group } from '../assignments/group.entity.js';
|
||||
|
||||
@Entity({ repository: () => QuestionRepository })
|
||||
export class Question {
|
||||
|
@ -15,11 +16,14 @@ export class Question {
|
|||
learningObjectLanguage!: Language;
|
||||
|
||||
@PrimaryKey({ type: 'number' })
|
||||
learningObjectVersion: number = 1;
|
||||
learningObjectVersion = 1;
|
||||
|
||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||
sequenceNumber?: number;
|
||||
|
||||
@ManyToOne({ entity: () => Group })
|
||||
inGroup!: Group;
|
||||
|
||||
@ManyToOne({
|
||||
entity: () => Student,
|
||||
})
|
||||
|
|
|
@ -8,17 +8,9 @@ import { StudentRepository } from '../../data/users/student-repository.js';
|
|||
repository: () => StudentRepository,
|
||||
})
|
||||
export class Student extends User {
|
||||
@ManyToMany(() => Class)
|
||||
@ManyToMany({ entity: () => Class, mappedBy: 'students' })
|
||||
classes!: Collection<Class>;
|
||||
|
||||
@ManyToMany(() => Group)
|
||||
groups!: Collection<Group>;
|
||||
|
||||
constructor(
|
||||
public username: string,
|
||||
public firstName: string,
|
||||
public lastName: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ManyToMany({ entity: () => Group, mappedBy: 'members' })
|
||||
groups: Collection<Group> = new Collection<Group>(this);
|
||||
}
|
||||
|
|
|
@ -5,14 +5,6 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js';
|
|||
|
||||
@Entity({ repository: () => TeacherRepository })
|
||||
export class Teacher extends User {
|
||||
@ManyToMany(() => Class)
|
||||
@ManyToMany({ entity: () => Class, mappedBy: 'teachers' })
|
||||
classes!: Collection<Class>;
|
||||
|
||||
constructor(
|
||||
public username: string,
|
||||
public firstName: string,
|
||||
public lastName: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ export abstract class User {
|
|||
username!: string;
|
||||
|
||||
@Property()
|
||||
firstName: string = '';
|
||||
firstName = '';
|
||||
|
||||
@Property()
|
||||
lastName: string = '';
|
||||
lastName = '';
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
/**
|
||||
* Exception for HTTP 400 Bad Request
|
||||
*/
|
||||
export class BadRequestException extends Error {
|
||||
public status = 400;
|
||||
|
||||
constructor(error: string) {
|
||||
super(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception for HTTP 401 Unauthorized
|
||||
*/
|
||||
export class UnauthorizedException extends Error {
|
||||
status = 401;
|
||||
constructor(message: string = 'Unauthorized') {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception for HTTP 403 Forbidden
|
||||
*/
|
||||
export class ForbiddenException extends Error {
|
||||
status = 403;
|
||||
|
||||
constructor(message: string = 'Forbidden') {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception for HTTP 404 Not Found
|
||||
*/
|
||||
export class NotFoundException extends Error {
|
||||
public status = 404;
|
||||
|
||||
constructor(error: string) {
|
||||
super(error);
|
||||
}
|
||||
}
|
10
backend/src/exceptions/bad-request-exception.ts
Normal file
10
backend/src/exceptions/bad-request-exception.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 400 Bad Request
|
||||
*/
|
||||
export class BadRequestException extends ExceptionWithHttpState {
|
||||
constructor(error: string) {
|
||||
super(400, error);
|
||||
}
|
||||
}
|
12
backend/src/exceptions/conflict-exception.ts
Normal file
12
backend/src/exceptions/conflict-exception.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 409 Conflict
|
||||
*/
|
||||
export class ConflictException extends ExceptionWithHttpState {
|
||||
public status = 409;
|
||||
|
||||
constructor(error: string) {
|
||||
super(409, error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { ConflictException } from './conflict-exception.js';
|
||||
|
||||
export class EntityAlreadyExistsException extends ConflictException {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
13
backend/src/exceptions/exception-with-http-state.ts
Normal file
13
backend/src/exceptions/exception-with-http-state.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { HasStatusCode } from './has-status-code';
|
||||
|
||||
/**
|
||||
* Exceptions which are associated with a HTTP error code.
|
||||
*/
|
||||
export abstract class ExceptionWithHttpState extends Error implements HasStatusCode {
|
||||
constructor(
|
||||
public status: number,
|
||||
public error: string
|
||||
) {
|
||||
super(error);
|
||||
}
|
||||
}
|
12
backend/src/exceptions/forbidden-exception.ts
Normal file
12
backend/src/exceptions/forbidden-exception.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 403 Forbidden
|
||||
*/
|
||||
export class ForbiddenException extends ExceptionWithHttpState {
|
||||
status = 403;
|
||||
|
||||
constructor(message = 'Forbidden') {
|
||||
super(403, message);
|
||||
}
|
||||
}
|
6
backend/src/exceptions/has-status-code.ts
Normal file
6
backend/src/exceptions/has-status-code.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export interface HasStatusCode {
|
||||
status: number;
|
||||
}
|
||||
export function hasStatusCode(err: unknown): err is HasStatusCode {
|
||||
return typeof err === 'object' && err !== null && 'status' in err && typeof (err as HasStatusCode)?.status === 'number';
|
||||
}
|
12
backend/src/exceptions/not-found-exception.ts
Normal file
12
backend/src/exceptions/not-found-exception.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 404 Not Found
|
||||
*/
|
||||
export class NotFoundException extends ExceptionWithHttpState {
|
||||
public status = 404;
|
||||
|
||||
constructor(error: string) {
|
||||
super(404, error);
|
||||
}
|
||||
}
|
12
backend/src/exceptions/server-error-exception.ts
Normal file
12
backend/src/exceptions/server-error-exception.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 500 Internal Server Error
|
||||
*/
|
||||
export class ServerErrorException extends ExceptionWithHttpState {
|
||||
status = 500;
|
||||
|
||||
constructor(message = 'Internal server error, something went wrong') {
|
||||
super(500, message);
|
||||
}
|
||||
}
|
10
backend/src/exceptions/unauthorized-exception.ts
Normal file
10
backend/src/exceptions/unauthorized-exception.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 401 Unauthorized
|
||||
*/
|
||||
export class UnauthorizedException extends ExceptionWithHttpState {
|
||||
constructor(message = 'Unauthorized') {
|
||||
super(401, message);
|
||||
}
|
||||
}
|
|
@ -1,21 +1,14 @@
|
|||
import { mapToUserDTO, UserDTO } from './user.js';
|
||||
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js';
|
||||
import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js';
|
||||
import { Answer } from '../entities/questions/answer.entity.js';
|
||||
|
||||
export interface AnswerDTO {
|
||||
author: UserDTO;
|
||||
toQuestion: QuestionDTO;
|
||||
sequenceNumber: number;
|
||||
timestamp: string;
|
||||
content: string;
|
||||
}
|
||||
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(),
|
||||
|
@ -23,16 +16,10 @@ export function mapToAnswerDTO(answer: Answer): AnswerDTO {
|
|||
};
|
||||
}
|
||||
|
||||
export interface AnswerId {
|
||||
author: string;
|
||||
toQuestion: QuestionId;
|
||||
sequenceNumber: number;
|
||||
}
|
||||
|
||||
export function mapToAnswerId(answer: AnswerDTO): AnswerId {
|
||||
export function mapToAnswerDTOId(answer: Answer): AnswerId {
|
||||
return {
|
||||
author: answer.author.username,
|
||||
toQuestion: mapToQuestionId(answer.toQuestion),
|
||||
sequenceNumber: answer.sequenceNumber,
|
||||
toQuestion: mapToQuestionDTOId(answer.toQuestion),
|
||||
sequenceNumber: answer.sequenceNumber!,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,50 +1,36 @@
|
|||
import { FALLBACK_LANG } from '../config.js';
|
||||
import { languageMap } from '@dwengo-1/common/util/language';
|
||||
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
||||
import { Class } from '../entities/classes/class.entity.js';
|
||||
import { languageMap } from '../entities/content/language.js';
|
||||
import { GroupDTO } from './group.js';
|
||||
import { AssignmentDTO, AssignmentDTOId } from '@dwengo-1/common/interfaces/assignment';
|
||||
import { mapToGroupDTO } from './group.js';
|
||||
import { getAssignmentRepository } from '../data/repositories.js';
|
||||
|
||||
export interface AssignmentDTO {
|
||||
id: number;
|
||||
class: string; // Id of class 'within'
|
||||
title: string;
|
||||
description: string;
|
||||
learningPath: string;
|
||||
language: string;
|
||||
groups?: GroupDTO[] | string[]; // TODO
|
||||
}
|
||||
|
||||
export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO {
|
||||
export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTOId {
|
||||
return {
|
||||
id: assignment.id!,
|
||||
class: assignment.within.classId!,
|
||||
title: assignment.title,
|
||||
description: assignment.description,
|
||||
learningPath: assignment.learningPathHruid,
|
||||
language: assignment.learningPathLanguage,
|
||||
// Groups: assignment.groups.map(group => group.groupNumber),
|
||||
within: assignment.within.classId!,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
|
||||
return {
|
||||
id: assignment.id!,
|
||||
class: assignment.within.classId!,
|
||||
within: assignment.within.classId!,
|
||||
title: assignment.title,
|
||||
description: assignment.description,
|
||||
learningPath: assignment.learningPathHruid,
|
||||
language: assignment.learningPathLanguage,
|
||||
// Groups: assignment.groups.map(mapToGroupDTO),
|
||||
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment {
|
||||
const assignment = new Assignment();
|
||||
assignment.title = assignmentData.title;
|
||||
assignment.description = assignmentData.description;
|
||||
assignment.learningPathHruid = assignmentData.learningPath;
|
||||
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
|
||||
assignment.within = cls;
|
||||
|
||||
return assignment;
|
||||
return getAssignmentRepository().create({
|
||||
within: cls,
|
||||
title: assignmentData.title,
|
||||
description: assignmentData.description,
|
||||
learningPathHruid: assignmentData.learningPath,
|
||||
learningPathLanguage: languageMap[assignmentData.language],
|
||||
groups: [],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,14 +2,7 @@ import { Collection } from '@mikro-orm/core';
|
|||
import { Class } from '../entities/classes/class.entity.js';
|
||||
import { Student } from '../entities/users/student.entity.js';
|
||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||
|
||||
export interface ClassDTO {
|
||||
id: string;
|
||||
displayName: string;
|
||||
teachers: string[];
|
||||
students: string[];
|
||||
joinRequests: string[];
|
||||
}
|
||||
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
|
||||
|
||||
export function mapToClassDTO(cls: Class): ClassDTO {
|
||||
return {
|
||||
|
@ -17,7 +10,6 @@ export function mapToClassDTO(cls: Class): ClassDTO {
|
|||
displayName: cls.displayName,
|
||||
teachers: cls.teachers.map((teacher) => teacher.username),
|
||||
students: cls.students.map((student) => student.username),
|
||||
joinRequests: [], // TODO
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,46 @@
|
|||
import { Group } from '../entities/assignments/group.entity.js';
|
||||
import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js';
|
||||
import { mapToStudentDTO, StudentDTO } from './student.js';
|
||||
import { mapToAssignment } from './assignment.js';
|
||||
import { mapToStudent } from './student.js';
|
||||
import { mapToStudentDTO } from './student.js';
|
||||
import { GroupDTO, GroupDTOId } from '@dwengo-1/common/interfaces/group';
|
||||
import { getGroupRepository } from '../data/repositories.js';
|
||||
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
|
||||
import { Class } from '../entities/classes/class.entity.js';
|
||||
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||
|
||||
export interface GroupDTO {
|
||||
assignment: number | AssignmentDTO;
|
||||
groupNumber: number;
|
||||
members: string[] | StudentDTO[];
|
||||
export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group {
|
||||
const assignmentDto = groupDto.assignment as AssignmentDTO;
|
||||
|
||||
return getGroupRepository().create({
|
||||
groupNumber: groupDto.groupNumber,
|
||||
assignment: mapToAssignment(assignmentDto, clazz),
|
||||
members: groupDto.members!.map((studentDto) => mapToStudent(studentDto as StudentDTO)),
|
||||
});
|
||||
}
|
||||
|
||||
export function mapToGroupDTO(group: Group): GroupDTO {
|
||||
export function mapToGroupDTO(group: Group, cls: Class): GroupDTO {
|
||||
return {
|
||||
assignment: mapToAssignmentDTO(group.assignment), // ERROR: , group.assignment.within),
|
||||
class: cls.classId!,
|
||||
assignment: group.assignment.id!,
|
||||
groupNumber: group.groupNumber!,
|
||||
members: group.members.map(mapToStudentDTO),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapToGroupDTOId(group: Group): GroupDTO {
|
||||
export function mapToGroupDTOId(group: Group, cls: Class): GroupDTOId {
|
||||
return {
|
||||
class: cls.classId!,
|
||||
assignment: group.assignment.id!,
|
||||
groupNumber: group.groupNumber!,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map to group DTO where other objects are only referenced by their id.
|
||||
*/
|
||||
export function mapToShallowGroupDTO(group: Group): GroupDTO {
|
||||
return {
|
||||
class: group.assignment.within.classId!,
|
||||
assignment: group.assignment.id!,
|
||||
groupNumber: group.groupNumber!,
|
||||
members: group.members.map((member) => member.username),
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
import { Language } from '../entities/content/language';
|
||||
|
||||
export interface Transition {
|
||||
default: boolean;
|
||||
_id: string;
|
||||
next: {
|
||||
_id: string;
|
||||
hruid: string;
|
||||
version: number;
|
||||
language: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LearningObjectIdentifier {
|
||||
hruid: string;
|
||||
language: Language;
|
||||
version?: number;
|
||||
}
|
||||
|
||||
export interface LearningObjectNode {
|
||||
_id: string;
|
||||
learningobject_hruid: string;
|
||||
version: number;
|
||||
language: Language;
|
||||
start_node?: boolean;
|
||||
transitions: Transition[];
|
||||
created_at: string;
|
||||
updatedAt: string;
|
||||
done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized.
|
||||
}
|
||||
|
||||
export interface LearningPath {
|
||||
_id: string;
|
||||
language: string;
|
||||
hruid: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string; // Image might be missing, so it's optional
|
||||
num_nodes: number;
|
||||
num_nodes_left: number;
|
||||
nodes: LearningObjectNode[];
|
||||
keywords: string;
|
||||
target_ages: number[];
|
||||
min_age: number;
|
||||
max_age: number;
|
||||
__order: number;
|
||||
}
|
||||
|
||||
export interface LearningPathIdentifier {
|
||||
hruid: string;
|
||||
language: Language;
|
||||
}
|
||||
|
||||
export interface EducationalGoal {
|
||||
source: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ReturnValue {
|
||||
callback_url: string;
|
||||
callback_schema: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface LearningObjectMetadata {
|
||||
_id: string;
|
||||
uuid: string;
|
||||
hruid: string;
|
||||
version: number;
|
||||
language: Language;
|
||||
title: string;
|
||||
description: string;
|
||||
difficulty: number;
|
||||
estimated_time: number;
|
||||
available: boolean;
|
||||
teacher_exclusive: boolean;
|
||||
educational_goals: EducationalGoal[];
|
||||
keywords: string[];
|
||||
target_ages: number[];
|
||||
content_type: string; // Markdown, image, etc.
|
||||
content_location?: string;
|
||||
skos_concepts?: string[];
|
||||
return_value?: ReturnValue;
|
||||
}
|
||||
|
||||
export interface FilteredLearningObject {
|
||||
key: string;
|
||||
_id: string;
|
||||
uuid: string;
|
||||
version: number;
|
||||
title: string;
|
||||
htmlUrl: string;
|
||||
language: Language;
|
||||
difficulty: number;
|
||||
estimatedTime?: number;
|
||||
available: boolean;
|
||||
teacherExclusive: boolean;
|
||||
educationalGoals: EducationalGoal[];
|
||||
keywords: string[];
|
||||
description: string;
|
||||
targetAges: number[];
|
||||
contentType: string;
|
||||
contentLocation?: string;
|
||||
skosConcepts?: string[];
|
||||
returnValue?: ReturnValue;
|
||||
}
|
||||
|
||||
export interface LearningPathResponse {
|
||||
success: boolean;
|
||||
source: string;
|
||||
data: LearningPath[] | null;
|
||||
message?: string;
|
||||
}
|
|
@ -1,42 +1,47 @@
|
|||
import { Question } from '../entities/questions/question.entity.js';
|
||||
import { mapToStudentDTO } from './student.js';
|
||||
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||
import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content';
|
||||
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||
import { mapToStudentDTO, StudentDTO } from './student.js';
|
||||
import { mapToGroupDTOId } from './group.js';
|
||||
|
||||
export interface QuestionDTO {
|
||||
learningObjectIdentifier: LearningObjectIdentifier;
|
||||
sequenceNumber?: number;
|
||||
author: StudentDTO;
|
||||
timestamp?: string;
|
||||
content: string;
|
||||
function getLearningObjectIdentifier(question: Question): LearningObjectIdentifierDTO {
|
||||
return {
|
||||
hruid: question.learningObjectHruid,
|
||||
language: question.learningObjectLanguage,
|
||||
version: question.learningObjectVersion,
|
||||
};
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
export function mapToQuestionDTO(question: Question): QuestionDTO {
|
||||
const learningObjectIdentifier = {
|
||||
hruid: question.learningObjectHruid,
|
||||
language: question.learningObjectLanguage,
|
||||
version: question.learningObjectVersion,
|
||||
};
|
||||
const learningObjectIdentifier = getLearningObjectIdentifier(question);
|
||||
|
||||
return {
|
||||
learningObjectIdentifier,
|
||||
sequenceNumber: question.sequenceNumber!,
|
||||
author: mapToStudentDTO(question.author),
|
||||
inGroup: mapToGroupDTOId(question.inGroup, question.inGroup.assignment?.within),
|
||||
timestamp: question.timestamp.toISOString(),
|
||||
content: question.content,
|
||||
};
|
||||
}
|
||||
|
||||
export interface QuestionId {
|
||||
learningObjectIdentifier: LearningObjectIdentifier;
|
||||
sequenceNumber: number;
|
||||
}
|
||||
export function mapToQuestionDTOId(question: Question): QuestionId {
|
||||
const learningObjectIdentifier = getLearningObjectIdentifier(question);
|
||||
|
||||
export function mapToQuestionId(question: QuestionDTO): QuestionId {
|
||||
return {
|
||||
learningObjectIdentifier: question.learningObjectIdentifier,
|
||||
learningObjectIdentifier,
|
||||
sequenceNumber: question.sequenceNumber!,
|
||||
};
|
||||
}
|
||||
|
|
23
backend/src/interfaces/student-request.ts
Normal file
23
backend/src/interfaces/student-request.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { mapToStudentDTO } from './student.js';
|
||||
import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js';
|
||||
import { getClassJoinRequestRepository } from '../data/repositories.js';
|
||||
import { Student } from '../entities/users/student.entity.js';
|
||||
import { Class } from '../entities/classes/class.entity.js';
|
||||
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||
|
||||
export function mapToStudentRequestDTO(request: ClassJoinRequest): ClassJoinRequestDTO {
|
||||
return {
|
||||
requester: mapToStudentDTO(request.requester),
|
||||
class: request.class.classId!,
|
||||
status: request.status,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapToStudentRequest(student: Student, cls: Class): ClassJoinRequest {
|
||||
return getClassJoinRequestRepository().create({
|
||||
requester: student,
|
||||
class: cls,
|
||||
status: ClassStatus.Open,
|
||||
});
|
||||
}
|
|
@ -1,17 +1,6 @@
|
|||
import { Student } from '../entities/users/student.entity.js';
|
||||
|
||||
export interface StudentDTO {
|
||||
id: string;
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
endpoints?: {
|
||||
classes: string;
|
||||
questions: string;
|
||||
invitations: string;
|
||||
groups: string;
|
||||
};
|
||||
}
|
||||
import { getStudentRepository } from '../data/repositories.js';
|
||||
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||
|
||||
export function mapToStudentDTO(student: Student): StudentDTO {
|
||||
return {
|
||||
|
@ -23,7 +12,9 @@ export function mapToStudentDTO(student: Student): StudentDTO {
|
|||
}
|
||||
|
||||
export function mapToStudent(studentData: StudentDTO): Student {
|
||||
const student = new Student(studentData.username, studentData.firstName, studentData.lastName);
|
||||
|
||||
return student;
|
||||
return getStudentRepository().create({
|
||||
username: studentData.username,
|
||||
firstName: studentData.firstName,
|
||||
lastName: studentData.lastName,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,26 +1,10 @@
|
|||
import { Submission } from '../entities/assignments/submission.entity.js';
|
||||
import { Language } from '../entities/content/language.js';
|
||||
import { GroupDTO, mapToGroupDTO } from './group.js';
|
||||
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js';
|
||||
import { LearningObjectIdentifier } from './learning-content.js';
|
||||
|
||||
export interface SubmissionDTO {
|
||||
learningObjectIdentifier: LearningObjectIdentifier;
|
||||
|
||||
submissionNumber?: number;
|
||||
submitter: StudentDTO;
|
||||
time?: Date;
|
||||
group?: GroupDTO;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface SubmissionDTOId {
|
||||
learningObjectHruid: string;
|
||||
learningObjectLanguage: Language;
|
||||
learningObjectVersion: number;
|
||||
|
||||
submissionNumber?: number;
|
||||
}
|
||||
import { mapToGroupDTOId } from './group.js';
|
||||
import { mapToStudentDTO } from './student.js';
|
||||
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
|
||||
import { getSubmissionRepository } from '../data/repositories.js';
|
||||
import { Student } from '../entities/users/student.entity.js';
|
||||
import { Group } from '../entities/assignments/group.entity.js';
|
||||
|
||||
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
||||
return {
|
||||
|
@ -29,11 +13,10 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
|||
language: submission.learningObjectLanguage,
|
||||
version: submission.learningObjectVersion,
|
||||
},
|
||||
|
||||
submissionNumber: submission.submissionNumber,
|
||||
submitter: mapToStudentDTO(submission.submitter),
|
||||
time: submission.submissionTime,
|
||||
group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf) : undefined,
|
||||
group: submission.onBehalfOf ? mapToGroupDTOId(submission.onBehalfOf, submission.onBehalfOf.assignment.within) : undefined,
|
||||
content: submission.content,
|
||||
};
|
||||
}
|
||||
|
@ -48,17 +31,14 @@ export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId {
|
|||
};
|
||||
}
|
||||
|
||||
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
|
||||
const submission = new Submission();
|
||||
submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid;
|
||||
submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language;
|
||||
submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!;
|
||||
// Submission.submissionNumber = submissionDTO.submissionNumber;
|
||||
submission.submitter = mapToStudent(submissionDTO.submitter);
|
||||
// Submission.submissionTime = submissionDTO.time;
|
||||
// Submission.onBehalfOf = submissionDTO.group!;
|
||||
// TODO fix group
|
||||
submission.content = submissionDTO.content;
|
||||
|
||||
return submission;
|
||||
export function mapToSubmission(submissionDTO: SubmissionDTO, submitter: Student, onBehalfOf: Group): Submission {
|
||||
return getSubmissionRepository().create({
|
||||
learningObjectHruid: submissionDTO.learningObjectIdentifier.hruid,
|
||||
learningObjectLanguage: submissionDTO.learningObjectIdentifier.language,
|
||||
learningObjectVersion: submissionDTO.learningObjectIdentifier.version || 1,
|
||||
submitter: submitter,
|
||||
submissionTime: new Date(),
|
||||
content: submissionDTO.content,
|
||||
onBehalfOf: onBehalfOf,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js';
|
||||
import { ClassDTO, mapToClassDTO } from './class.js';
|
||||
import { mapToUserDTO, UserDTO } from './user.js';
|
||||
|
||||
export interface TeacherInvitationDTO {
|
||||
sender: string | UserDTO;
|
||||
receiver: string | UserDTO;
|
||||
class: string | ClassDTO;
|
||||
}
|
||||
import { mapToUserDTO } from './user.js';
|
||||
import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation';
|
||||
import { getTeacherInvitationRepository } from '../data/repositories.js';
|
||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||
import { Class } from '../entities/classes/class.entity.js';
|
||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||
|
||||
export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO {
|
||||
return {
|
||||
sender: mapToUserDTO(invitation.sender),
|
||||
receiver: mapToUserDTO(invitation.receiver),
|
||||
class: mapToClassDTO(invitation.class),
|
||||
classId: invitation.class.classId!,
|
||||
status: invitation.status,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -20,6 +19,16 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea
|
|||
return {
|
||||
sender: invitation.sender.username,
|
||||
receiver: invitation.receiver.username,
|
||||
class: invitation.class.classId!,
|
||||
classId: invitation.class.classId!,
|
||||
status: invitation.status,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapToInvitation(sender: Teacher, receiver: Teacher, cls: Class): TeacherInvitation {
|
||||
return getTeacherInvitationRepository().create({
|
||||
sender,
|
||||
receiver,
|
||||
class: cls,
|
||||
status: ClassStatus.Open,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,17 +1,6 @@
|
|||
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||
|
||||
export interface TeacherDTO {
|
||||
id: string;
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
endpoints?: {
|
||||
classes: string;
|
||||
questions: string;
|
||||
invitations: string;
|
||||
groups: string;
|
||||
};
|
||||
}
|
||||
import { getTeacherRepository } from '../data/repositories.js';
|
||||
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
||||
|
||||
export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
|
||||
return {
|
||||
|
@ -22,8 +11,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
|
|||
};
|
||||
}
|
||||
|
||||
export function mapToTeacher(TeacherData: TeacherDTO): Teacher {
|
||||
const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName);
|
||||
|
||||
return teacher;
|
||||
export function mapToTeacher(teacherData: TeacherDTO): Teacher {
|
||||
return getTeacherRepository().create({
|
||||
username: teacherData.username,
|
||||
firstName: teacherData.firstName,
|
||||
lastName: teacherData.lastName,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,17 +1,5 @@
|
|||
import { User } from '../entities/users/user.entity.js';
|
||||
|
||||
export interface UserDTO {
|
||||
id?: string;
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
endpoints?: {
|
||||
self: string;
|
||||
classes: string;
|
||||
questions: string;
|
||||
invitations: string;
|
||||
};
|
||||
}
|
||||
import { UserDTO } from '@dwengo-1/common/interfaces/user';
|
||||
|
||||
export function mapToUserDTO(user: User): UserDTO {
|
||||
return {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
|
||||
import LokiTransport from 'winston-loki';
|
||||
import { LokiLabels } from 'loki-logger-ts';
|
||||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
||||
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||
|
||||
export class Logger extends WinstonLogger {
|
||||
constructor() {
|
||||
|
@ -9,7 +9,7 @@ export class Logger extends WinstonLogger {
|
|||
}
|
||||
}
|
||||
|
||||
const Labels: LokiLabels = {
|
||||
const lokiLabels: LokiLabels = {
|
||||
source: 'Dwengo-Backend',
|
||||
service: 'API',
|
||||
host: 'localhost',
|
||||
|
@ -22,28 +22,30 @@ function initializeLogger(): Logger {
|
|||
return logger;
|
||||
}
|
||||
|
||||
const logLevel = getEnvVar(EnvVars.LogLevel);
|
||||
const logLevel = getEnvVar(envVars.LogLevel);
|
||||
|
||||
const consoleTransport = new transports.Console({
|
||||
level: getEnvVar(EnvVars.LogLevel),
|
||||
format: format.combine(format.cli(), format.colorize()),
|
||||
level: getEnvVar(envVars.LogLevel),
|
||||
format: format.combine(format.cli(), format.simple()),
|
||||
});
|
||||
|
||||
if (getEnvVar(EnvVars.RunMode) === 'dev') {
|
||||
return createLogger({
|
||||
if (getEnvVar(envVars.RunMode) === 'dev') {
|
||||
logger = createLogger({
|
||||
transports: [consoleTransport],
|
||||
});
|
||||
logger.debug(`Logger initialized with level ${logLevel} to console`);
|
||||
return logger;
|
||||
}
|
||||
|
||||
const lokiHost = getEnvVar(EnvVars.LokiHost);
|
||||
const lokiHost = getEnvVar(envVars.LokiHost);
|
||||
|
||||
const lokiTransport: LokiTransport = new LokiTransport({
|
||||
host: lokiHost,
|
||||
labels: Labels,
|
||||
labels: lokiLabels,
|
||||
level: logLevel,
|
||||
json: true,
|
||||
format: format.combine(format.timestamp(), format.json()),
|
||||
onConnectionError: (err) => {
|
||||
onConnectionError: (err): void => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Connection error: ${err}`);
|
||||
},
|
||||
|
|
|
@ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts';
|
|||
export class MikroOrmLogger extends DefaultLogger {
|
||||
private logger: Logger = getLogger();
|
||||
|
||||
log(namespace: LoggerNamespace, message: string, context?: LogContext) {
|
||||
static createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown {
|
||||
const labels: LokiLabels = {
|
||||
service: 'ORM',
|
||||
};
|
||||
|
||||
let message: string;
|
||||
if (context !== undefined && context.labels !== undefined) {
|
||||
message = `[${namespace}] (${context.label}) ${messageArg}`;
|
||||
} else {
|
||||
message = `[${namespace}] ${messageArg}`;
|
||||
}
|
||||
|
||||
return {
|
||||
message: message,
|
||||
labels: labels,
|
||||
context: context,
|
||||
};
|
||||
}
|
||||
|
||||
log(namespace: LoggerNamespace, message: string, context?: LogContext): void {
|
||||
if (!this.isEnabled(namespace, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (namespace) {
|
||||
case 'query':
|
||||
this.logger.debug(this.createMessage(namespace, message, context));
|
||||
this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
|
||||
break;
|
||||
case 'query-params':
|
||||
// TODO Which log level should this be?
|
||||
this.logger.info(this.createMessage(namespace, message, context));
|
||||
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||
break;
|
||||
case 'schema':
|
||||
this.logger.info(this.createMessage(namespace, message, context));
|
||||
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||
break;
|
||||
case 'discovery':
|
||||
this.logger.debug(this.createMessage(namespace, message, context));
|
||||
this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
|
||||
break;
|
||||
case 'info':
|
||||
this.logger.info(this.createMessage(namespace, message, context));
|
||||
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||
break;
|
||||
case 'deprecated':
|
||||
this.logger.warn(this.createMessage(namespace, message, context));
|
||||
this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context));
|
||||
break;
|
||||
default:
|
||||
switch (context?.level) {
|
||||
case 'info':
|
||||
this.logger.info(this.createMessage(namespace, message, context));
|
||||
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||
break;
|
||||
case 'warning':
|
||||
this.logger.warn(message);
|
||||
|
@ -47,23 +66,4 @@ export class MikroOrmLogger extends DefaultLogger {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) {
|
||||
const labels: LokiLabels = {
|
||||
service: 'ORM',
|
||||
};
|
||||
|
||||
let message: string;
|
||||
if (context?.label) {
|
||||
message = `[${namespace}] (${context?.label}) ${messageArg}`;
|
||||
} else {
|
||||
message = `[${namespace}] ${messageArg}`;
|
||||
}
|
||||
|
||||
return {
|
||||
message: message,
|
||||
labels: labels,
|
||||
context: context,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getLogger, Logger } from './initalize.js';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export function responseTimeLogger(req: Request, res: Response, time: number) {
|
||||
export function responseTimeLogger(req: Request, res: Response, time: number): void {
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
const method = req.method;
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { EnvVars, getEnvVar } from '../../util/envvars.js';
|
||||
import { envVars, getEnvVar } from '../../util/envVars.js';
|
||||
import { expressjwt } from 'express-jwt';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { JwtPayload } from 'jsonwebtoken';
|
||||
import jwksClient from 'jwks-rsa';
|
||||
import * as express from 'express';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { AuthenticatedRequest } from './authenticated-request.js';
|
||||
import { AuthenticationInfo } from './authentication-info.js';
|
||||
import { ForbiddenException, UnauthorizedException } from '../../exceptions.js';
|
||||
import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js';
|
||||
import { ForbiddenException } from '../../exceptions/forbidden-exception.js';
|
||||
|
||||
const JWKS_CACHE = true;
|
||||
const JWKS_RATE_LIMIT = true;
|
||||
|
@ -32,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient {
|
|||
|
||||
const idpConfigs = {
|
||||
student: {
|
||||
issuer: getEnvVar(EnvVars.IdpStudentUrl),
|
||||
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)),
|
||||
issuer: getEnvVar(envVars.IdpStudentUrl),
|
||||
jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)),
|
||||
},
|
||||
teacher: {
|
||||
issuer: getEnvVar(EnvVars.IdpTeacherUrl),
|
||||
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)),
|
||||
issuer: getEnvVar(envVars.IdpTeacherUrl),
|
||||
jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -47,14 +48,14 @@ const idpConfigs = {
|
|||
const verifyJwtToken = expressjwt({
|
||||
secret: async (_: express.Request, token: jwt.Jwt | undefined) => {
|
||||
if (!token?.payload || !(token.payload as JwtPayload).iss) {
|
||||
throw new Error('Invalid token');
|
||||
throw new UnauthorizedException('Invalid token.');
|
||||
}
|
||||
|
||||
const issuer = (token.payload as JwtPayload).iss;
|
||||
|
||||
const idpConfig = Object.values(idpConfigs).find((config) => config.issuer === issuer);
|
||||
if (!idpConfig) {
|
||||
throw new Error('Issuer not accepted.');
|
||||
throw new UnauthorizedException('Issuer not accepted.');
|
||||
}
|
||||
|
||||
const signingKey = await idpConfig.jwksClient.getSigningKey(token.header.kid);
|
||||
|
@ -63,7 +64,7 @@ const verifyJwtToken = expressjwt({
|
|||
}
|
||||
return signingKey.getPublicKey();
|
||||
},
|
||||
audience: getEnvVar(EnvVars.IdpAudience),
|
||||
audience: getEnvVar(envVars.IdpAudience),
|
||||
algorithms: [JWT_ALGORITHM],
|
||||
credentialsRequired: false,
|
||||
requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD,
|
||||
|
@ -74,7 +75,7 @@ const verifyJwtToken = expressjwt({
|
|||
*/
|
||||
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
|
||||
if (!req.jwtPayload) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
const issuer = req.jwtPayload.iss;
|
||||
let accountType: 'student' | 'teacher';
|
||||
|
@ -84,8 +85,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
|
|||
} else if (issuer === idpConfigs.teacher.issuer) {
|
||||
accountType = 'teacher';
|
||||
} else {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
accountType: accountType,
|
||||
username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!,
|
||||
|
@ -100,10 +102,10 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
|
|||
* Add the AuthenticationInfo object with the information about the current authentication to the request in order
|
||||
* to avoid that the routers have to deal with the JWT token.
|
||||
*/
|
||||
const addAuthenticationInfo = (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => {
|
||||
function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void {
|
||||
req.auth = getAuthenticationInfo(req);
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
||||
|
||||
|
@ -113,9 +115,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
|||
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
|
||||
* to true.
|
||||
*/
|
||||
export const authorize =
|
||||
(accessCondition: (auth: AuthenticationInfo) => boolean) =>
|
||||
(req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => {
|
||||
export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) {
|
||||
return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
|
||||
if (!req.auth) {
|
||||
throw new UnauthorizedException();
|
||||
} else if (!accessCondition(req.auth)) {
|
||||
|
@ -124,6 +125,7 @@ export const authorize =
|
|||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware which rejects all unauthenticated users, but accepts all authenticated users.
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* Object with information about the user who is currently logged in.
|
||||
*/
|
||||
export type AuthenticationInfo = {
|
||||
export interface AuthenticationInfo {
|
||||
accountType: 'student' | 'teacher';
|
||||
username: string;
|
||||
name?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import cors from 'cors';
|
||||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
||||
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||
|
||||
export default cors({
|
||||
origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','),
|
||||
allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','),
|
||||
origin: getEnvVar(envVars.CorsAllowedOrigins).split(','),
|
||||
allowedHeaders: getEnvVar(envVars.CorsAllowedHeaders).split(','),
|
||||
});
|
||||
|
|
15
backend/src/middleware/error-handling/error-handler.ts
Normal file
15
backend/src/middleware/error-handling/error-handler.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { NextFunction, Request, Response } from 'express';
|
||||
import { getLogger, Logger } from '../../logging/initalize.js';
|
||||
import { hasStatusCode } from '../../exceptions/has-status-code.js';
|
||||
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void {
|
||||
if (hasStatusCode(err)) {
|
||||
logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`);
|
||||
res.status(err.status).json(err);
|
||||
} else {
|
||||
logger.error(`Unexpected error occurred while handing a request: ${(err as { stack: string })?.stack ?? JSON.stringify(err)}`);
|
||||
res.status(500).json(err);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { LoggerOptions, Options } from '@mikro-orm/core';
|
||||
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
|
||||
import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js';
|
||||
import { envVars, getEnvVar, getNumericEnvVar } from './util/envVars.js';
|
||||
import { SqliteDriver } from '@mikro-orm/sqlite';
|
||||
import { MikroOrmLogger } from './logging/mikroOrmLogger.js';
|
||||
|
||||
|
@ -42,33 +42,35 @@ const entities = [
|
|||
Question,
|
||||
];
|
||||
|
||||
function config(testingMode: boolean = false): Options {
|
||||
function config(testingMode = false): Options {
|
||||
if (testingMode) {
|
||||
return {
|
||||
driver: SqliteDriver,
|
||||
dbName: getEnvVar(EnvVars.DbName),
|
||||
dbName: getEnvVar(envVars.DbName),
|
||||
subscribers: [new SqliteAutoincrementSubscriber()],
|
||||
entities: entities,
|
||||
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
|
||||
// EntitiesTs: entitiesTs,
|
||||
|
||||
// Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
|
||||
// (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
|
||||
dynamicImportProvider: (id) => import(id),
|
||||
dynamicImportProvider: async (id) => import(id),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
driver: PostgreSqlDriver,
|
||||
host: getEnvVar(EnvVars.DbHost),
|
||||
port: getNumericEnvVar(EnvVars.DbPort),
|
||||
dbName: getEnvVar(EnvVars.DbName),
|
||||
user: getEnvVar(EnvVars.DbUsername),
|
||||
password: getEnvVar(EnvVars.DbPassword),
|
||||
host: getEnvVar(envVars.DbHost),
|
||||
port: getNumericEnvVar(envVars.DbPort),
|
||||
dbName: getEnvVar(envVars.DbName),
|
||||
user: getEnvVar(envVars.DbUsername),
|
||||
password: getEnvVar(envVars.DbPassword),
|
||||
entities: entities,
|
||||
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
|
||||
// EntitiesTs: entitiesTs,
|
||||
|
||||
// Logging
|
||||
debug: getEnvVar(EnvVars.LogLevel) === 'debug',
|
||||
debug: getEnvVar(envVars.LogLevel) === 'debug',
|
||||
loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 { envVars, getEnvVar } from './util/envVars.js';
|
||||
import { getLogger, Logger } from './logging/initalize.js';
|
||||
|
||||
let orm: MikroORM | undefined;
|
||||
export async function initORM(testingMode: boolean = false) {
|
||||
export async function initORM(testingMode = false): Promise<MikroORM<IDatabaseDriver, EntityManager>> {
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
logger.info('Initializing ORM');
|
||||
|
@ -12,7 +12,7 @@ export async function initORM(testingMode: boolean = false) {
|
|||
|
||||
orm = await MikroORM.init(config(testingMode));
|
||||
// Update the database scheme if necessary and enabled.
|
||||
if (getEnvVar(EnvVars.DbUpdate)) {
|
||||
if (getEnvVar(envVars.DbUpdate)) {
|
||||
await orm.schema.updateSchema();
|
||||
} else {
|
||||
const diff = await orm.schema.getUpdateSchemaSQL();
|
||||
|
@ -25,6 +25,8 @@ export async function initORM(testingMode: boolean = false) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,29 +1,30 @@
|
|||
import express from 'express';
|
||||
import {
|
||||
createAssignmentHandler,
|
||||
deleteAssignmentHandler,
|
||||
getAllAssignmentsHandler,
|
||||
getAssignmentHandler,
|
||||
getAssignmentQuestionsHandler,
|
||||
getAssignmentsSubmissionsHandler,
|
||||
putAssignmentHandler,
|
||||
} from '../controllers/assignments.js';
|
||||
import groupRouter from './groups.js';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
// Root endpoint used to search objects
|
||||
router.get('/', getAllAssignmentsHandler);
|
||||
|
||||
router.post('/', createAssignmentHandler);
|
||||
|
||||
// Information about an assignment with id 'id'
|
||||
router.get('/:id', getAssignmentHandler);
|
||||
|
||||
router.put('/:id', putAssignmentHandler);
|
||||
|
||||
router.delete('/:id', deleteAssignmentHandler);
|
||||
|
||||
router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
|
||||
|
||||
router.get('/:id/questions', (req, res) => {
|
||||
res.json({
|
||||
questions: ['0'],
|
||||
});
|
||||
});
|
||||
router.get('/:id/questions', getAssignmentQuestionsHandler);
|
||||
|
||||
router.use('/:assignmentid/groups', groupRouter);
|
||||
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
import express from 'express';
|
||||
import { getFrontendAuthConfig } from '../controllers/auth.js';
|
||||
import { getFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js';
|
||||
import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js';
|
||||
const router = express.Router();
|
||||
|
||||
// Returns auth configuration for frontend
|
||||
router.get('/config', (req, res) => {
|
||||
router.get('/config', (_req, res) => {
|
||||
res.json(getFrontendAuthConfig());
|
||||
});
|
||||
|
||||
router.get('/testAuthenticatedOnly', authenticatedOnly, (req, res) => {
|
||||
router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => {
|
||||
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
|
||||
res.json({ message: 'If you see this, you should be authenticated!' });
|
||||
});
|
||||
|
||||
router.get('/testStudentsOnly', studentsOnly, (req, res) => {
|
||||
router.get('/testStudentsOnly', studentsOnly, (_req, res) => {
|
||||
/* #swagger.security = [{ "student": [ ] }] */
|
||||
res.json({ message: 'If you see this, you should be a student!' });
|
||||
});
|
||||
|
||||
router.get('/testTeachersOnly', teachersOnly, (req, res) => {
|
||||
router.get('/testTeachersOnly', teachersOnly, (_req, res) => {
|
||||
/* #swagger.security = [{ "teacher": [ ] }] */
|
||||
res.json({ message: 'If you see this, you should be a teacher!' });
|
||||
});
|
||||
|
||||
router.post('/hello', authenticatedOnly, postHelloHandler);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import express from 'express';
|
||||
import {
|
||||
addClassStudentHandler,
|
||||
addClassTeacherHandler,
|
||||
createClassHandler,
|
||||
deleteClassHandler,
|
||||
deleteClassStudentHandler,
|
||||
deleteClassTeacherHandler,
|
||||
getAllClassesHandler,
|
||||
getClassHandler,
|
||||
getClassStudentsHandler,
|
||||
getClassTeachersHandler,
|
||||
getTeacherInvitationsHandler,
|
||||
putClassHandler,
|
||||
} from '../controllers/classes.js';
|
||||
import assignmentRouter from './assignments.js';
|
||||
|
||||
|
@ -15,13 +22,26 @@ router.get('/', getAllClassesHandler);
|
|||
|
||||
router.post('/', createClassHandler);
|
||||
|
||||
// Information about an class with id 'id'
|
||||
router.get('/:id', getClassHandler);
|
||||
|
||||
router.put('/:id', putClassHandler);
|
||||
|
||||
router.delete('/:id', deleteClassHandler);
|
||||
|
||||
router.get('/:id/teacher-invitations', getTeacherInvitationsHandler);
|
||||
|
||||
router.get('/:id/students', getClassStudentsHandler);
|
||||
|
||||
router.post('/:id/students', addClassStudentHandler);
|
||||
|
||||
router.delete('/:id/students/:username', deleteClassStudentHandler);
|
||||
|
||||
router.get('/:id/teachers', getClassTeachersHandler);
|
||||
|
||||
router.post('/:id/teachers', addClassTeacherHandler);
|
||||
|
||||
router.delete('/:id/teachers/:username', deleteClassTeacherHandler);
|
||||
|
||||
router.use('/:classid/assignments', assignmentRouter);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import express from 'express';
|
||||
import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js';
|
||||
import {
|
||||
createGroupHandler,
|
||||
deleteGroupHandler,
|
||||
getAllGroupsHandler,
|
||||
getGroupHandler,
|
||||
getGroupQuestionsHandler,
|
||||
getGroupSubmissionsHandler,
|
||||
putGroupHandler,
|
||||
} from '../controllers/groups.js';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
|
@ -8,16 +16,14 @@ router.get('/', getAllGroupsHandler);
|
|||
|
||||
router.post('/', createGroupHandler);
|
||||
|
||||
// Information about a group (members, ... [TODO DOC])
|
||||
router.get('/:groupid', getGroupHandler);
|
||||
|
||||
router.get('/:groupid', getGroupSubmissionsHandler);
|
||||
router.put('/:groupid', putGroupHandler);
|
||||
|
||||
// The list of questions a group has made
|
||||
router.get('/:id/questions', (req, res) => {
|
||||
res.json({
|
||||
questions: ['0'],
|
||||
});
|
||||
});
|
||||
router.delete('/:groupid', deleteGroupHandler);
|
||||
|
||||
router.get('/:groupid/submissions', getGroupSubmissionsHandler);
|
||||
|
||||
router.get('/:groupid/questions', getGroupQuestionsHandler);
|
||||
|
||||
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;
|
||||
|
|
19
backend/src/routes/student-join-requests.ts
Normal file
19
backend/src/routes/student-join-requests.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import express from 'express';
|
||||
import {
|
||||
createStudentRequestHandler,
|
||||
deleteClassJoinRequestHandler,
|
||||
getStudentRequestHandler,
|
||||
getStudentRequestsHandler,
|
||||
} from '../controllers/students.js';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get('/', getStudentRequestsHandler);
|
||||
|
||||
router.post('/', createStudentRequestHandler);
|
||||
|
||||
router.get('/:classId', getStudentRequestHandler);
|
||||
|
||||
router.delete('/:classId', deleteClassJoinRequestHandler);
|
||||
|
||||
export default router;
|
|
@ -7,8 +7,11 @@ import {
|
|||
getStudentClassesHandler,
|
||||
getStudentGroupsHandler,
|
||||
getStudentHandler,
|
||||
getStudentQuestionsHandler,
|
||||
getStudentSubmissionsHandler,
|
||||
} from '../controllers/students.js';
|
||||
import joinRequestRouter from './student-join-requests.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Root endpoint used to search objects
|
||||
|
@ -16,30 +19,26 @@ router.get('/', getAllStudentsHandler);
|
|||
|
||||
router.post('/', createStudentHandler);
|
||||
|
||||
router.delete('/', deleteStudentHandler);
|
||||
|
||||
router.delete('/:username', deleteStudentHandler);
|
||||
|
||||
// Information about a student's profile
|
||||
router.get('/:username', getStudentHandler);
|
||||
|
||||
// The list of classes a student is in
|
||||
router.get('/:id/classes', getStudentClassesHandler);
|
||||
router.get('/:username/classes', getStudentClassesHandler);
|
||||
|
||||
// The list of submissions a student has made
|
||||
router.get('/:id/submissions', getStudentSubmissionsHandler);
|
||||
router.get('/:username/submissions', getStudentSubmissionsHandler);
|
||||
|
||||
// The list of assignments a student has
|
||||
router.get('/:id/assignments', getStudentAssignmentsHandler);
|
||||
router.get('/:username/assignments', getStudentAssignmentsHandler);
|
||||
|
||||
// The list of groups a student is in
|
||||
router.get('/:id/groups', getStudentGroupsHandler);
|
||||
router.get('/:username/groups', getStudentGroupsHandler);
|
||||
|
||||
// A list of questions a user has created
|
||||
router.get('/:id/questions', (req, res) => {
|
||||
res.json({
|
||||
questions: ['0'],
|
||||
});
|
||||
});
|
||||
router.get('/:username/questions', getStudentQuestionsHandler);
|
||||
|
||||
router.use('/:username/joinRequests', joinRequestRouter);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import express from 'express';
|
||||
import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler } from '../controllers/submissions.js';
|
||||
import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.js';
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
// Root endpoint used to search objects
|
||||
router.get('/', (req, res) => {
|
||||
res.json({
|
||||
submissions: ['0', '1'],
|
||||
});
|
||||
});
|
||||
router.get('/', getSubmissionsHandler);
|
||||
|
||||
router.post('/:id', createSubmissionHandler);
|
||||
router.post('/', createSubmissionHandler);
|
||||
|
||||
// Information about an submission with id 'id'
|
||||
router.get('/:id', getSubmissionHandler);
|
||||
|
|
22
backend/src/routes/teacher-invitations.ts
Normal file
22
backend/src/routes/teacher-invitations.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import express from 'express';
|
||||
import {
|
||||
createInvitationHandler,
|
||||
deleteInvitationHandler,
|
||||
getAllInvitationsHandler,
|
||||
getInvitationHandler,
|
||||
updateInvitationHandler,
|
||||
} from '../controllers/teacher-invitations.js';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get('/:username', getAllInvitationsHandler);
|
||||
|
||||
router.get('/:sender/:receiver/:classId', getInvitationHandler);
|
||||
|
||||
router.post('/', createInvitationHandler);
|
||||
|
||||
router.put('/', updateInvitationHandler);
|
||||
|
||||
router.delete('/:sender/:receiver/:classId', deleteInvitationHandler);
|
||||
|
||||
export default router;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue