Merge branch 'github-actions/deployment' into chore/github-actions
This commit is contained in:
commit
b13b9dc674
107 changed files with 5474 additions and 602 deletions
7
.dockerignore
Normal file
7
.dockerignore
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
**/node_modules/
|
||||||
|
**/dist
|
||||||
|
.git
|
||||||
|
npm-debug.log
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.env
|
19
.github/workflows/deployment.yml
vendored
Normal file
19
.github/workflows/deployment.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
name: Deployment
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
name: Deploy with docker
|
||||||
|
runs-on: [self-hosted, Linux, X64]
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Start docker
|
||||||
|
run: docker compose -f compose.yml -f compose.prod.yml up --build -d
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
DWENGO_PORT=3000
|
#
|
||||||
|
# Basic configuration
|
||||||
|
#
|
||||||
|
|
||||||
|
DWENGO_PORT=3000 # The port the backend will listen on
|
||||||
DWENGO_DB_HOST=localhost
|
DWENGO_DB_HOST=localhost
|
||||||
DWENGO_DB_PORT=5431
|
DWENGO_DB_PORT=5431
|
||||||
DWENGO_DB_USERNAME=postgres
|
DWENGO_DB_USERNAME=postgres
|
||||||
DWENGO_DB_PASSWORD=postgres
|
DWENGO_DB_PASSWORD=postgres
|
||||||
DWENGO_DB_UPDATE=true
|
DWENGO_DB_UPDATE=true
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
|
||||||
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
|
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
|
||||||
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||||
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs
|
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs
|
||||||
|
@ -14,3 +20,9 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/
|
||||||
|
|
||||||
# Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production!
|
# 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:5173
|
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173
|
||||||
|
|
||||||
|
#
|
||||||
|
# Advanced configuration
|
||||||
|
#
|
||||||
|
|
||||||
|
# LOKI_HOST=http://localhost:9001 # The address of the Loki instance, used for logging
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
#
|
||||||
|
# Basic configuration
|
||||||
|
#
|
||||||
|
|
||||||
DWENGO_PORT=3000 # The port the backend will listen on
|
DWENGO_PORT=3000 # The port the backend will listen on
|
||||||
DWENGO_DB_HOST=domain-or-ip-of-database
|
DWENGO_DB_HOST=domain-or-ip-of-database
|
||||||
DWENGO_DB_PORT=5432
|
DWENGO_DB_PORT=5431
|
||||||
|
|
||||||
# Change this to the actual credentials of the user Dwengo should use in the backend
|
# Change this to the actual credentials of the user Dwengo should use in the backend
|
||||||
DWENGO_DB_USERNAME=postgres
|
DWENGO_DB_USERNAME=postgres
|
||||||
|
@ -19,4 +23,5 @@ DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
|
||||||
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||||
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
|
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
|
||||||
|
|
||||||
# LOKI_HOST=http://localhost:3102 # The address of the Loki instance, used for logging
|
# The address of the Lokiinstance, used for logging
|
||||||
|
# LOKI_HOST=http://localhost:3102
|
||||||
|
|
28
backend/.env.production.example
Normal file
28
backend/.env.production.example
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
DWENGO_PORT=3000 # The port the backend will listen on
|
||||||
|
DWENGO_DB_HOST=db # Name of the database container
|
||||||
|
DWENGO_DB_PORT=5431
|
||||||
|
|
||||||
|
# Change this to the actual credentials of the user Dwengo should use in the backend
|
||||||
|
DWENGO_DB_NAME=postgres
|
||||||
|
DWENGO_DB_USERNAME=postgres
|
||||||
|
DWENGO_DB_PASSWORD=postgres
|
||||||
|
|
||||||
|
# Set this to true when the database scheme needs to be updated. In that case, take a backup first.
|
||||||
|
DWENGO_DB_UPDATE=false
|
||||||
|
|
||||||
|
# Data for the identity provider via which the students authenticate.
|
||||||
|
DWENGO_AUTH_STUDENT_URL=https://sel2-1.ugent.be/idp/realms/student
|
||||||
|
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||||
|
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs # Name of the idp container
|
||||||
|
# Data for the identity provider via which the teachers authenticate.
|
||||||
|
DWENGO_AUTH_TEACHER_URL=https://sel2-1.ugent.be/idp/realms/teacher
|
||||||
|
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||||
|
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container
|
||||||
|
|
||||||
|
#
|
||||||
|
# Advanced configuration
|
||||||
|
#
|
||||||
|
|
||||||
|
# Logging and monitoring
|
||||||
|
|
||||||
|
# LOKI_HOST=http://logging:3102 # The address of the Loki instance, used for logging
|
37
backend/Dockerfile
Normal file
37
backend/Dockerfile
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
FROM node:22 AS build-stage
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY backend/package.json ./backend/
|
||||||
|
|
||||||
|
RUN npm install --silent
|
||||||
|
|
||||||
|
# Build the backend
|
||||||
|
|
||||||
|
# Root tsconfig.json
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
|
||||||
|
WORKDIR /app/backend
|
||||||
|
|
||||||
|
COPY backend ./
|
||||||
|
COPY docs /app/docs
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22 AS production-stage
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package-lock.json backend/package.json ./
|
||||||
|
|
||||||
|
RUN npm install --silent --only=production
|
||||||
|
|
||||||
|
COPY ./docs /docs
|
||||||
|
COPY --from=build-stage /app/backend/dist ./dist/
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "--env-file=.env", "dist/app.js"]
|
7
backend/config.js
Normal file
7
backend/config.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// 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;
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dwengo-1-backend",
|
"name": "dwengo-1-backend",
|
||||||
"version": "0.0.1",
|
"version": "0.1.1",
|
||||||
"description": "Backend for Dwengo-1",
|
"description": "Backend for Dwengo-1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -34,6 +34,7 @@
|
||||||
"loki-logger-ts": "^1.0.2",
|
"loki-logger-ts": "^1.0.2",
|
||||||
"marked": "^15.0.7",
|
"marked": "^15.0.7",
|
||||||
"response-time": "^2.3.3",
|
"response-time": "^2.3.3",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-loki": "^6.1.3"
|
"winston-loki": "^6.1.3"
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^22.13.4",
|
"@types/node": "^22.13.4",
|
||||||
"@types/response-time": "^2.3.8",
|
"@types/response-time": "^2.3.8",
|
||||||
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
|
|
|
@ -1,58 +1,36 @@
|
||||||
import express, { Express, Response } from 'express';
|
import express, { Express } from 'express';
|
||||||
import { initORM } from './orm.js';
|
import { initORM } from './orm.js';
|
||||||
|
|
||||||
import themeRoutes from './routes/themes.js';
|
|
||||||
import learningPathRoutes from './routes/learning-paths.js';
|
|
||||||
import learningObjectRoutes from './routes/learning-objects.js';
|
|
||||||
|
|
||||||
import studentRouter from './routes/student.js';
|
|
||||||
import groupRouter from './routes/group.js';
|
|
||||||
import assignmentRouter from './routes/assignment.js';
|
|
||||||
import submissionRouter from './routes/submission.js';
|
|
||||||
import classRouter from './routes/class.js';
|
|
||||||
import questionRouter from './routes/question.js';
|
|
||||||
import authRouter from './routes/auth.js';
|
|
||||||
import { authenticateUser } from './middleware/auth/auth.js';
|
import { authenticateUser } from './middleware/auth/auth.js';
|
||||||
import cors from './middleware/cors.js';
|
import cors from './middleware/cors.js';
|
||||||
import { getLogger, Logger } from './logging/initalize.js';
|
import { getLogger, Logger } from './logging/initalize.js';
|
||||||
import { responseTimeLogger } from './logging/responseTimeLogger.js';
|
import { responseTimeLogger } from './logging/responseTimeLogger.js';
|
||||||
import responseTime from 'response-time';
|
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';
|
||||||
|
|
||||||
const logger: Logger = getLogger();
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
const app: Express = express();
|
const app: Express = express();
|
||||||
const port: string | number = getNumericEnvVar(EnvVars.Port);
|
const port: string | number = getNumericEnvVar(EnvVars.Port);
|
||||||
|
|
||||||
app.use(cors);
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(responseTime(responseTimeLogger));
|
app.use(cors);
|
||||||
app.use(authenticateUser);
|
app.use(authenticateUser);
|
||||||
|
// Add response time logging
|
||||||
|
app.use(responseTime(responseTimeLogger));
|
||||||
|
|
||||||
// TODO Replace with Express routes
|
app.use('/api', apiRouter);
|
||||||
app.get('/', (_, res: Response) => {
|
|
||||||
logger.debug('GET /');
|
|
||||||
res.json({
|
|
||||||
message: 'Hello Dwengo!🚀',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use('/student', studentRouter);
|
// Swagger
|
||||||
app.use('/group', groupRouter);
|
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
|
||||||
app.use('/assignment', assignmentRouter);
|
|
||||||
app.use('/submission', submissionRouter);
|
|
||||||
app.use('/class', classRouter);
|
|
||||||
app.use('/question', questionRouter);
|
|
||||||
app.use('/auth', authRouter);
|
|
||||||
app.use('/theme', themeRoutes);
|
|
||||||
app.use('/learningPath', learningPathRoutes);
|
|
||||||
app.use('/learningObject', learningObjectRoutes);
|
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
await initORM();
|
await initORM();
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
logger.info(`Server is running at http://localhost:${port}`);
|
logger.info(`Server is running at http://localhost:${port}/api`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { EnvVars, getEnvVar } from './util/envvars.js';
|
import { EnvVars, getEnvVar } from './util/envvars.js';
|
||||||
|
import { Language } from './entities/content/language.js';
|
||||||
|
|
||||||
// API
|
// API
|
||||||
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
|
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
|
||||||
|
@ -7,3 +8,5 @@ export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage);
|
||||||
// Logging
|
// Logging
|
||||||
export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info';
|
export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info';
|
||||||
export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102';
|
export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102';
|
||||||
|
|
||||||
|
export const FALLBACK_SEQ_NUM = 1;
|
||||||
|
|
76
backend/src/controllers/assignments.ts
Normal file
76
backend/src/controllers/assignments.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js';
|
||||||
|
import { AssignmentDTO } from '../interfaces/assignment.js';
|
||||||
|
|
||||||
|
// Typescript is annoy with with parameter forwarding from class.ts
|
||||||
|
interface AssignmentParams {
|
||||||
|
classid: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllAssignmentsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
||||||
|
const classid = req.params.classid;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
const assignments = await getAllAssignments(classid, full);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
assignments: assignments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
||||||
|
const classid = req.params.classid;
|
||||||
|
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: assignment });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
||||||
|
const id = +req.params.id;
|
||||||
|
const classid = req.params.classid;
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignment = await getAssignment(classid, id);
|
||||||
|
|
||||||
|
if (!assignment) {
|
||||||
|
res.status(404).json({ error: 'Assignment not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(assignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
||||||
|
const classid = req.params.classid;
|
||||||
|
const assignmentNumber = +req.params.id;
|
||||||
|
|
||||||
|
if (isNaN(assignmentNumber)) {
|
||||||
|
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
submissions: submissions,
|
||||||
|
});
|
||||||
|
}
|
77
backend/src/controllers/classes.ts
Normal file
77
backend/src/controllers/classes.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/class.js';
|
||||||
|
import { ClassDTO } from '../interfaces/class.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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createClassHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
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({ class: cls });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClassHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const classId = req.params.id;
|
||||||
|
const cls = await getClass(classId);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
res.status(404).json({ error: 'Class not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cls.endpoints = {
|
||||||
|
self: `${req.baseUrl}/${req.params.id}`,
|
||||||
|
invitations: `${req.baseUrl}/${req.params.id}/invitations`,
|
||||||
|
assignments: `${req.baseUrl}/${req.params.id}/assignments`,
|
||||||
|
students: `${req.baseUrl}/${req.params.id}/students`,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(cls);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching learning objects:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.id;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
const students = full ? await getClassStudents(classId) : await getClassStudentsIds(classId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
students: students,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.id;
|
||||||
|
const full = req.query.full === 'true'; // TODO: not implemented yet
|
||||||
|
|
||||||
|
const invitations = await getClassTeacherInvitations(classId, full);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
invitations: invitations,
|
||||||
|
});
|
||||||
|
}
|
95
backend/src/controllers/groups.ts
Normal file
95
backend/src/controllers/groups.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js';
|
||||||
|
import { GroupDTO } from '../interfaces/group.js';
|
||||||
|
|
||||||
|
// Typescript is annoywith with parameter forwarding from class.ts
|
||||||
|
interface GroupParams {
|
||||||
|
classid: string;
|
||||||
|
assignmentid: string;
|
||||||
|
groupid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGroupHandler(req: Request<GroupParams>, 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 group = await getGroup(classId, assignmentId, groupId, full);
|
||||||
|
|
||||||
|
res.json(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllGroupsHandler(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 groups = await getAllGroups(classId, assignmentId, full);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
groups: groups,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createGroupHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classid = req.params.classid;
|
||||||
|
const assignmentId = +req.params.assignmentid;
|
||||||
|
|
||||||
|
if (isNaN(assignmentId)) {
|
||||||
|
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: group });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 submissions = await getGroupSubmissions(classId, assignmentId, groupId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
submissions: submissions,
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,45 +0,0 @@
|
||||||
import { Request, Response } from 'express';
|
|
||||||
import { themes } from '../data/themes.js';
|
|
||||||
import { FALLBACK_LANG } from '../config.js';
|
|
||||||
import { getLogger } from '../logging/initalize.js';
|
|
||||||
import learningPathService from '../services/learning-paths/learning-path-service.js';
|
|
||||||
import { Language } from '../entities/content/language.js';
|
|
||||||
/**
|
|
||||||
* Fetch learning paths based on query parameters.
|
|
||||||
*/
|
|
||||||
export async function getLearningPaths(req: Request, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const hruids = req.query.hruid;
|
|
||||||
const themeKey = req.query.theme as string;
|
|
||||||
const searchQuery = req.query.search as string;
|
|
||||||
const language = (req.query.language as Language) || FALLBACK_LANG;
|
|
||||||
|
|
||||||
let hruidList;
|
|
||||||
|
|
||||||
if (hruids) {
|
|
||||||
hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)];
|
|
||||||
} else if (themeKey) {
|
|
||||||
const theme = themes.find((t) => t.title === themeKey);
|
|
||||||
if (theme) {
|
|
||||||
hruidList = theme.hruids;
|
|
||||||
} else {
|
|
||||||
res.status(404).json({
|
|
||||||
error: `Theme "${themeKey}" not found.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (searchQuery) {
|
|
||||||
const searchResults = await learningPathService.searchLearningPaths(searchQuery, language);
|
|
||||||
res.json(searchResults);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
hruidList = themes.flatMap((theme) => theme.hruids);
|
|
||||||
}
|
|
||||||
|
|
||||||
const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language, `HRUIDs: ${hruidList.join(', ')}`);
|
|
||||||
res.json(learningPaths.data);
|
|
||||||
} catch (error) {
|
|
||||||
getLogger().error('❌ Unexpected error fetching learning paths:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
119
backend/src/controllers/questions.ts
Normal file
119
backend/src/controllers/questions.ts
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
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 { 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hruid,
|
||||||
|
language: (lang as Language) || FALLBACK_LANG,
|
||||||
|
version: +version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQuestionId(req: Request, res: Response): QuestionId | null {
|
||||||
|
const seq = req.params.seq;
|
||||||
|
const learningObjectIdentifier = getObjectId(req, res);
|
||||||
|
|
||||||
|
if (!learningObjectIdentifier) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
learningObjectIdentifier,
|
||||||
|
sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const objectId = getObjectId(req, res);
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
if (!objectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const questions = await getAllQuestions(objectId, full);
|
||||||
|
|
||||||
|
if (!questions) {
|
||||||
|
res.status(404).json({ error: `Questions not found.` });
|
||||||
|
} else {
|
||||||
|
res.json(questions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const questionId = getQuestionId(req, res);
|
||||||
|
|
||||||
|
if (!questionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = getAnswersByQuestion(questionId, full);
|
||||||
|
|
||||||
|
if (!answers) {
|
||||||
|
res.status(404).json({ error: `Questions not found.` });
|
||||||
|
} else {
|
||||||
|
res.json(answers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const questionDTO = req.body as QuestionDTO;
|
||||||
|
|
||||||
|
if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) {
|
||||||
|
res.status(400).json({ error: 'Missing required fields: identifier and content' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const question = await createQuestion(questionDTO);
|
||||||
|
|
||||||
|
if (!question) {
|
||||||
|
res.status(400).json({ error: 'Could not add question' });
|
||||||
|
} else {
|
||||||
|
res.json(question);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const questionId = getQuestionId(req, res);
|
||||||
|
|
||||||
|
if (!questionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const question = await deleteQuestion(questionId);
|
||||||
|
|
||||||
|
if (!question) {
|
||||||
|
res.status(400).json({ error: 'Could not find nor delete question' });
|
||||||
|
} else {
|
||||||
|
res.json(question);
|
||||||
|
}
|
||||||
|
}
|
146
backend/src/controllers/students.ts
Normal file
146
backend/src/controllers/students.ts
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import {
|
||||||
|
createStudent,
|
||||||
|
deleteStudent,
|
||||||
|
getAllStudents,
|
||||||
|
getStudent,
|
||||||
|
getStudentAssignments,
|
||||||
|
getStudentClasses,
|
||||||
|
getStudentGroups,
|
||||||
|
getStudentSubmissions,
|
||||||
|
} from '../services/students.js';
|
||||||
|
import { ClassDTO } from '../interfaces/class.js';
|
||||||
|
import { getAllAssignments } from '../services/assignments.js';
|
||||||
|
import { getUserHandler } from './users.js';
|
||||||
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
|
import { StudentDTO } from '../interfaces/student.js';
|
||||||
|
import { getStudentRepository } from '../data/repositories.js';
|
||||||
|
import { UserDTO } from '../interfaces/user.js';
|
||||||
|
|
||||||
|
// TODO: accept arguments (full, ...)
|
||||||
|
// TODO: endpoints
|
||||||
|
export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
|
||||||
|
const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents();
|
||||||
|
|
||||||
|
if (!students) {
|
||||||
|
res.status(404).json({ error: `Student not found.` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(students);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const username = req.params.username;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getStudent(username);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({
|
||||||
|
error: `User with username '${username}' not found.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStudentHandler(req: Request, res: Response) {
|
||||||
|
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);
|
||||||
|
res.status(201).json(newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteStudentHandler(req: Request, res: Response) {
|
||||||
|
const username = req.params.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
const username = req.params.id;
|
||||||
|
|
||||||
|
const classes = await getStudentClasses(username, full);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
classes: classes,
|
||||||
|
endpoints: {
|
||||||
|
self: `${req.baseUrl}/${req.params.id}`,
|
||||||
|
classes: `${req.baseUrl}/${req.params.id}/invitations`,
|
||||||
|
questions: `${req.baseUrl}/${req.params.id}/assignments`,
|
||||||
|
students: `${req.baseUrl}/${req.params.id}/students`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching learning objects:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// Might not be fully correct depending on if
|
||||||
|
// A class has an assignment, that all students
|
||||||
|
// 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 assignments = getStudentAssignments(username, full);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
assignments: assignments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
const username = req.params.id;
|
||||||
|
|
||||||
|
const groups = await getStudentGroups(username, full);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
groups: groups,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const username = req.params.id;
|
||||||
|
|
||||||
|
const submissions = await getStudentSubmissions(username);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
submissions: submissions,
|
||||||
|
});
|
||||||
|
}
|
59
backend/src/controllers/submissions.ts
Normal file
59
backend/src/controllers/submissions.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js';
|
||||||
|
import { Language, languageMap } from '../entities/content/language.js';
|
||||||
|
import { SubmissionDTO } from '../interfaces/submission';
|
||||||
|
|
||||||
|
interface SubmissionParams {
|
||||||
|
hruid: string;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSubmissionHandler(req: Request<SubmissionParams>, 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 submission = await getSubmission(lohruid, lang, version, submissionNumber);
|
||||||
|
|
||||||
|
if (!submission) {
|
||||||
|
res.status(404).json({ error: 'Submission not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(submission);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSubmissionHandler(req: Request, res: Response) {
|
||||||
|
const submissionDTO = req.body as SubmissionDTO;
|
||||||
|
|
||||||
|
const submission = await createSubmission(submissionDTO);
|
||||||
|
|
||||||
|
if (!submission) {
|
||||||
|
res.status(404).json({ error: 'Submission not added' });
|
||||||
|
} else {
|
||||||
|
res.json(submission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSubmissionHandler(req: Request, res: Response) {
|
||||||
|
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 submission = await deleteSubmission(hruid, lang, version, submissionNumber);
|
||||||
|
|
||||||
|
if (!submission) {
|
||||||
|
res.status(404).json({ error: 'Submission not found' });
|
||||||
|
} else {
|
||||||
|
res.json(submission);
|
||||||
|
}
|
||||||
|
}
|
144
backend/src/controllers/teachers.ts
Normal file
144
backend/src/controllers/teachers.ts
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import {
|
||||||
|
createTeacher,
|
||||||
|
deleteTeacher,
|
||||||
|
getAllTeachers,
|
||||||
|
getClassesByTeacher,
|
||||||
|
getClassIdsByTeacher,
|
||||||
|
getQuestionIdsByTeacher,
|
||||||
|
getQuestionsByTeacher,
|
||||||
|
getStudentIdsByTeacher,
|
||||||
|
getStudentsByTeacher,
|
||||||
|
getTeacher,
|
||||||
|
} from '../services/teachers.js';
|
||||||
|
import { ClassDTO } from '../interfaces/class.js';
|
||||||
|
import { StudentDTO } from '../interfaces/student.js';
|
||||||
|
import { QuestionDTO, QuestionId } from '../interfaces/question.js';
|
||||||
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
|
import { TeacherDTO } from '../interfaces/teacher.js';
|
||||||
|
import { getTeacherRepository } from '../data/repositories.js';
|
||||||
|
|
||||||
|
export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
const teacherRepository = getTeacherRepository();
|
||||||
|
|
||||||
|
const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers();
|
||||||
|
|
||||||
|
if (!teachers) {
|
||||||
|
res.status(404).json({ error: `Teacher not found.` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(teachers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const username = req.params.username;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getTeacher(username);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({
|
||||||
|
error: `User with username '${username}' not found.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTeacherHandler(req: Request, res: Response) {
|
||||||
|
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);
|
||||||
|
res.status(201).json(newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTeacherHandler(req: Request, res: Response) {
|
||||||
|
const username = req.params.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 with username '${username}' not found.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(deletedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const username = req.params.username as string;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes: ClassDTO[] | string[] = full ? await getClassesByTeacher(username) : await getClassIdsByTeacher(username);
|
||||||
|
|
||||||
|
res.status(201).json(classes);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching classes by teacher:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const username = req.params.username as string;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const students: StudentDTO[] | string[] = full ? await getStudentsByTeacher(username) : await getStudentIdsByTeacher(username);
|
||||||
|
|
||||||
|
res.status(201).json(students);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching students by teacher:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const username = req.params.username as string;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const questions: QuestionDTO[] | QuestionId[] = full ? await getQuestionsByTeacher(username) : await getQuestionIdsByTeacher(username);
|
||||||
|
|
||||||
|
res.status(201).json(questions);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching questions by teacher:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { themes } from '../data/themes.js';
|
import { themes } from '../data/themes.js';
|
||||||
import { loadTranslations } from '../util/translationHelper.js';
|
import { loadTranslations } from '../util/translation-helper.js';
|
||||||
|
|
||||||
interface Translations {
|
interface Translations {
|
||||||
curricula_page: {
|
curricula_page: {
|
||||||
|
|
91
backend/src/controllers/users.ts
Normal file
91
backend/src/controllers/users.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { UserService } from '../services/users.js';
|
||||||
|
import { UserDTO } from '../interfaces/user.js';
|
||||||
|
import { User } from '../entities/users/user.entity.js';
|
||||||
|
|
||||||
|
export async function getAllUsersHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
const users: UserDTO[] | string[] = full ? await service.getAllUsers() : await service.getAllUserIds();
|
||||||
|
|
||||||
|
if (!users) {
|
||||||
|
res.status(404).json({ error: `Users not found.` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(users);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error fetching users:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const username = req.params.username as string;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await service.getUserByUsername(username);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({
|
||||||
|
error: `User with username '${username}' not found.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error fetching users:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>, UserClass: new () => T) {
|
||||||
|
try {
|
||||||
|
console.log('req', req);
|
||||||
|
const userData = req.body as UserDTO;
|
||||||
|
|
||||||
|
if (!userData.username || !userData.firstName || !userData.lastName) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Missing required fields: username, firstName, lastName',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = await service.createUser(userData, UserClass);
|
||||||
|
res.status(201).json(newUser);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error creating user:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>) {
|
||||||
|
try {
|
||||||
|
const username = req.params.username;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedUser = await service.deleteUser(username);
|
||||||
|
if (!deletedUser) {
|
||||||
|
res.status(404).json({
|
||||||
|
error: `User with username '${username}' not found.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(deletedUser);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error deleting user:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,26 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { Group } from '../../entities/assignments/group.entity.js';
|
import { Group } from '../../entities/assignments/group.entity.js';
|
||||||
import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
||||||
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
|
|
||||||
export class GroupRepository extends DwengoEntityRepository<Group> {
|
export class GroupRepository extends DwengoEntityRepository<Group> {
|
||||||
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
|
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
|
||||||
return this.findOne({
|
return this.findOne(
|
||||||
|
{
|
||||||
assignment: assignment,
|
assignment: assignment,
|
||||||
groupNumber: groupNumber,
|
groupNumber: groupNumber,
|
||||||
});
|
},
|
||||||
|
{ populate: ['members'] }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
|
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
|
||||||
return this.findAll({ where: { assignment: assignment } });
|
return this.findAll({
|
||||||
|
where: { assignment: assignment },
|
||||||
|
populate: ['members'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public findAllGroupsWithStudent(student: Student): Promise<Group[]> {
|
||||||
|
return this.find({ members: student }, { populate: ['members'] });
|
||||||
}
|
}
|
||||||
public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) {
|
public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
|
|
|
@ -38,6 +38,14 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
|
||||||
|
return this.find({ onBehalfOf: group });
|
||||||
|
}
|
||||||
|
|
||||||
|
public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
|
||||||
|
return this.find({ submitter: student });
|
||||||
|
}
|
||||||
|
|
||||||
public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
|
public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
|
|
|
@ -1,11 +1,23 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { Class } from '../../entities/classes/class.entity.js';
|
import { Class } from '../../entities/classes/class.entity.js';
|
||||||
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
|
import { Teacher } from '../../entities/users/teacher.entity';
|
||||||
|
|
||||||
export class ClassRepository extends DwengoEntityRepository<Class> {
|
export class ClassRepository extends DwengoEntityRepository<Class> {
|
||||||
public findById(id: string): Promise<Class | null> {
|
public findById(id: string): Promise<Class | null> {
|
||||||
return this.findOne({ classId: id });
|
return this.findOne({ classId: id }, { populate: ['students', 'teachers'] });
|
||||||
}
|
}
|
||||||
public deleteById(id: string): Promise<void> {
|
public deleteById(id: string): Promise<void> {
|
||||||
return this.deleteWhere({ classId: id });
|
return this.deleteWhere({ classId: id });
|
||||||
}
|
}
|
||||||
|
public 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[]> {
|
||||||
|
return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
import { Language } from '../../entities/content/language';
|
import { Language } from '../../entities/content/language.js';
|
||||||
|
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||||
|
|
||||||
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
||||||
public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||||
|
@ -31,4 +32,11 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
||||||
|
return this.find(
|
||||||
|
{ admins: teacher },
|
||||||
|
{ populate: ['admins'] } // Make sure to load admin relations
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { Question } from '../../entities/questions/question.entity.js';
|
import { Question } from '../../entities/questions/question.entity.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
|
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||||
|
|
||||||
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||||
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
|
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
|
||||||
|
@ -40,4 +41,17 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||||
sequenceNumber: sequenceNumber,
|
sequenceNumber: sequenceNumber,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async findAllByLearningObjects(learningObjects: LearningObject[]): Promise<Question[]> {
|
||||||
|
const objectIdentifiers = learningObjects.map((lo) => ({
|
||||||
|
learningObjectHruid: lo.hruid,
|
||||||
|
learningObjectLanguage: lo.language,
|
||||||
|
learningObjectVersion: lo.version,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return this.findAll({
|
||||||
|
where: { $or: objectIdentifiers },
|
||||||
|
orderBy: { timestamp: 'ASC' },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,6 @@ function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(en
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Users */
|
/* Users */
|
||||||
export const getUserRepository = repositoryGetter<User, UserRepository>(User);
|
|
||||||
export const getStudentRepository = repositoryGetter<Student, StudentRepository>(Student);
|
export const getStudentRepository = repositoryGetter<Student, StudentRepository>(Student);
|
||||||
export const getTeacherRepository = repositoryGetter<Teacher, TeacherRepository>(Teacher);
|
export const getTeacherRepository = repositoryGetter<Teacher, TeacherRepository>(Teacher);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
|
import { User } from '../../entities/users/user.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> {
|
export class StudentRepository extends DwengoEntityRepository<Student> {
|
||||||
public findByUsername(username: string): Promise<Student | null> {
|
public findByUsername(username: string): Promise<Student | null> {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||||
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
|
import { UserRepository } from './user-repository.js';
|
||||||
|
|
||||||
export class TeacherRepository extends DwengoEntityRepository<Teacher> {
|
export class TeacherRepository extends DwengoEntityRepository<Teacher> {
|
||||||
public findByUsername(username: string): Promise<Teacher | null> {
|
public findByUsername(username: string): Promise<Teacher | null> {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { User } from '../../entities/users/user.entity.js';
|
import { User } from '../../entities/users/user.entity.js';
|
||||||
|
|
||||||
export class UserRepository extends DwengoEntityRepository<User> {
|
export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
|
||||||
public findByUsername(username: string): Promise<User | null> {
|
public findByUsername(username: string): Promise<T | null> {
|
||||||
return this.findOne({ username: username });
|
return this.findOne({ username } as Partial<T>);
|
||||||
}
|
}
|
||||||
public deleteByUsername(username: string): Promise<void> {
|
public deleteByUsername(username: string): Promise<void> {
|
||||||
return this.deleteWhere({ username: username });
|
return this.deleteWhere({ username } as Partial<T>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
import { Class } from '../classes/class.entity.js';
|
import { Class } from '../classes/class.entity.js';
|
||||||
import { Group } from './group.entity.js';
|
import { Group } from './group.entity.js';
|
||||||
import { Language } from '../content/language.js';
|
import { Language } from '../content/language.js';
|
||||||
import { AssignmentRepository } from '../../data/assignments/assignment-repository.js';
|
import { AssignmentRepository } from '../../data/assignments/assignment-repository.js';
|
||||||
|
|
||||||
@Entity({ repository: () => AssignmentRepository })
|
@Entity({
|
||||||
|
repository: () => AssignmentRepository,
|
||||||
|
})
|
||||||
export class Assignment {
|
export class Assignment {
|
||||||
@ManyToOne({ entity: () => Class, primary: true })
|
@ManyToOne({
|
||||||
|
entity: () => Class,
|
||||||
|
primary: true,
|
||||||
|
})
|
||||||
within!: Class;
|
within!: Class;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'number' })
|
@PrimaryKey({ type: 'number', autoincrement: true })
|
||||||
id!: number;
|
id?: number;
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
title!: string;
|
title!: string;
|
||||||
|
@ -21,9 +26,14 @@ export class Assignment {
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
learningPathHruid!: string;
|
learningPathHruid!: string;
|
||||||
|
|
||||||
@Enum({ items: () => Language })
|
@Enum({
|
||||||
|
items: () => Language,
|
||||||
|
})
|
||||||
learningPathLanguage!: Language;
|
learningPathLanguage!: Language;
|
||||||
|
|
||||||
@OneToMany({ entity: () => Group, mappedBy: 'assignment' })
|
@OneToMany({
|
||||||
|
entity: () => Group,
|
||||||
|
mappedBy: 'assignment',
|
||||||
|
})
|
||||||
groups!: Group[];
|
groups!: Group[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
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 { Assignment } from './assignment.entity.js';
|
||||||
import { Student } from '../users/student.entity.js';
|
import { Student } from '../users/student.entity.js';
|
||||||
import { GroupRepository } from '../../data/assignments/group-repository.js';
|
import { GroupRepository } from '../../data/assignments/group-repository.js';
|
||||||
|
|
||||||
@Entity({ repository: () => GroupRepository })
|
@Entity({
|
||||||
|
repository: () => GroupRepository,
|
||||||
|
})
|
||||||
export class Group {
|
export class Group {
|
||||||
@ManyToOne({
|
@ManyToOne({
|
||||||
entity: () => Assignment,
|
entity: () => Assignment,
|
||||||
|
@ -11,8 +13,8 @@ export class Group {
|
||||||
})
|
})
|
||||||
assignment!: Assignment;
|
assignment!: Assignment;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'integer' })
|
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||||
groupNumber!: number;
|
groupNumber?: number;
|
||||||
|
|
||||||
@ManyToMany({
|
@ManyToMany({
|
||||||
entity: () => Student,
|
entity: () => Student,
|
||||||
|
|
|
@ -18,7 +18,7 @@ export class Submission {
|
||||||
@PrimaryKey({ type: 'numeric' })
|
@PrimaryKey({ type: 'numeric' })
|
||||||
learningObjectVersion: number = 1;
|
learningObjectVersion: number = 1;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'integer' })
|
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||||
submissionNumber!: number;
|
submissionNumber!: number;
|
||||||
|
|
||||||
@ManyToOne({
|
@ManyToOne({
|
||||||
|
|
|
@ -3,7 +3,9 @@ import { Student } from '../users/student.entity.js';
|
||||||
import { Class } from './class.entity.js';
|
import { Class } from './class.entity.js';
|
||||||
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
|
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
|
||||||
|
|
||||||
@Entity({ repository: () => ClassJoinRequestRepository })
|
@Entity({
|
||||||
|
repository: () => ClassJoinRequestRepository,
|
||||||
|
})
|
||||||
export class ClassJoinRequest {
|
export class ClassJoinRequest {
|
||||||
@ManyToOne({
|
@ManyToOne({
|
||||||
entity: () => Student,
|
entity: () => Student,
|
||||||
|
|
|
@ -4,10 +4,12 @@ import { Teacher } from '../users/teacher.entity.js';
|
||||||
import { Student } from '../users/student.entity.js';
|
import { Student } from '../users/student.entity.js';
|
||||||
import { ClassRepository } from '../../data/classes/class-repository.js';
|
import { ClassRepository } from '../../data/classes/class-repository.js';
|
||||||
|
|
||||||
@Entity({ repository: () => ClassRepository })
|
@Entity({
|
||||||
|
repository: () => ClassRepository,
|
||||||
|
})
|
||||||
export class Class {
|
export class Class {
|
||||||
@PrimaryKey()
|
@PrimaryKey()
|
||||||
classId = v4();
|
classId? = v4();
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
displayName!: string;
|
displayName!: string;
|
||||||
|
|
|
@ -7,9 +7,6 @@ import { TeacherInvitationRepository } from '../../data/classes/teacher-invitati
|
||||||
* Invitation of a teacher into a class (in order to teach it).
|
* Invitation of a teacher into a class (in order to teach it).
|
||||||
*/
|
*/
|
||||||
@Entity({ repository: () => TeacherInvitationRepository })
|
@Entity({ repository: () => TeacherInvitationRepository })
|
||||||
@Entity({
|
|
||||||
repository: () => TeacherInvitationRepository,
|
|
||||||
})
|
|
||||||
export class TeacherInvitation {
|
export class TeacherInvitation {
|
||||||
@ManyToOne({
|
@ManyToOne({
|
||||||
entity: () => Teacher,
|
entity: () => Teacher,
|
||||||
|
|
|
@ -2,7 +2,9 @@ import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
import { LearningObject } from './learning-object.entity.js';
|
import { LearningObject } from './learning-object.entity.js';
|
||||||
import { AttachmentRepository } from '../../data/content/attachment-repository.js';
|
import { AttachmentRepository } from '../../data/content/attachment-repository.js';
|
||||||
|
|
||||||
@Entity({ repository: () => AttachmentRepository })
|
@Entity({
|
||||||
|
repository: () => AttachmentRepository,
|
||||||
|
})
|
||||||
export class Attachment {
|
export class Attachment {
|
||||||
@ManyToOne({
|
@ManyToOne({
|
||||||
entity: () => LearningObject,
|
entity: () => LearningObject,
|
||||||
|
|
|
@ -184,3 +184,10 @@ export enum Language {
|
||||||
Zhuang = 'za',
|
Zhuang = 'za',
|
||||||
Zulu = 'zu',
|
Zulu = 'zu',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const languageMap: Record<string, Language> = {
|
||||||
|
nl: Language.Dutch,
|
||||||
|
fr: Language.French,
|
||||||
|
en: Language.English,
|
||||||
|
de: Language.German,
|
||||||
|
};
|
||||||
|
|
38
backend/src/interfaces/answer.ts
Normal file
38
backend/src/interfaces/answer.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { mapToUserDTO, UserDTO } from './user.js';
|
||||||
|
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js';
|
||||||
|
import { Answer } from '../entities/questions/answer.entity.js';
|
||||||
|
|
||||||
|
export interface AnswerDTO {
|
||||||
|
author: UserDTO;
|
||||||
|
toQuestion: QuestionDTO;
|
||||||
|
sequenceNumber: number;
|
||||||
|
timestamp: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Question entity to a DTO format.
|
||||||
|
*/
|
||||||
|
export function mapToAnswerDTO(answer: Answer): AnswerDTO {
|
||||||
|
return {
|
||||||
|
author: mapToUserDTO(answer.author),
|
||||||
|
toQuestion: mapToQuestionDTO(answer.toQuestion),
|
||||||
|
sequenceNumber: answer.sequenceNumber!,
|
||||||
|
timestamp: answer.timestamp.toISOString(),
|
||||||
|
content: answer.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnswerId {
|
||||||
|
author: string;
|
||||||
|
toQuestion: QuestionId;
|
||||||
|
sequenceNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToAnswerId(answer: AnswerDTO): AnswerId {
|
||||||
|
return {
|
||||||
|
author: answer.author.username,
|
||||||
|
toQuestion: mapToQuestionId(answer.toQuestion),
|
||||||
|
sequenceNumber: answer.sequenceNumber,
|
||||||
|
};
|
||||||
|
}
|
52
backend/src/interfaces/assignment.ts
Normal file
52
backend/src/interfaces/assignment.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { FALLBACK_LANG } from '../config.js';
|
||||||
|
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, mapToGroupDTO } from './group.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 {
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
|
||||||
|
return {
|
||||||
|
id: assignment.id!,
|
||||||
|
class: assignment.within.classId!,
|
||||||
|
title: assignment.title,
|
||||||
|
description: assignment.description,
|
||||||
|
learningPath: assignment.learningPathHruid,
|
||||||
|
language: assignment.learningPathLanguage,
|
||||||
|
// Groups: assignment.groups.map(mapToGroupDTO),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
console.log(assignment);
|
||||||
|
|
||||||
|
return assignment;
|
||||||
|
}
|
37
backend/src/interfaces/class.ts
Normal file
37
backend/src/interfaces/class.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
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[];
|
||||||
|
endpoints?: {
|
||||||
|
self: string;
|
||||||
|
invitations: string;
|
||||||
|
assignments: string;
|
||||||
|
students: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToClassDTO(cls: Class): ClassDTO {
|
||||||
|
return {
|
||||||
|
id: cls.classId!,
|
||||||
|
displayName: cls.displayName,
|
||||||
|
teachers: cls.teachers.map((teacher) => teacher.username),
|
||||||
|
students: cls.students.map((student) => student.username),
|
||||||
|
joinRequests: [], // TODO
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToClass(classData: ClassDTO, students: Collection<Student>, teachers: Collection<Teacher>): Class {
|
||||||
|
const cls = new Class();
|
||||||
|
cls.displayName = classData.displayName;
|
||||||
|
cls.students = students;
|
||||||
|
cls.teachers = teachers;
|
||||||
|
|
||||||
|
return cls;
|
||||||
|
}
|
25
backend/src/interfaces/group.ts
Normal file
25
backend/src/interfaces/group.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { Group } from '../entities/assignments/group.entity.js';
|
||||||
|
import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js';
|
||||||
|
import { mapToStudentDTO, StudentDTO } from './student.js';
|
||||||
|
|
||||||
|
export interface GroupDTO {
|
||||||
|
assignment: number | AssignmentDTO;
|
||||||
|
groupNumber: number;
|
||||||
|
members: string[] | StudentDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToGroupDTO(group: Group): GroupDTO {
|
||||||
|
return {
|
||||||
|
assignment: mapToAssignmentDTO(group.assignment), // ERROR: , group.assignment.within),
|
||||||
|
groupNumber: group.groupNumber!,
|
||||||
|
members: group.members.map(mapToStudentDTO),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToGroupDTOId(group: Group): GroupDTO {
|
||||||
|
return {
|
||||||
|
assignment: group.assignment.id!,
|
||||||
|
groupNumber: group.groupNumber!,
|
||||||
|
members: group.members.map((member) => member.username),
|
||||||
|
};
|
||||||
|
}
|
5
backend/src/interfaces/list.ts
Normal file
5
backend/src/interfaces/list.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// TODO: implement something like this but with named endpoints
|
||||||
|
export interface List<T> {
|
||||||
|
items: T[];
|
||||||
|
endpoints?: string[];
|
||||||
|
}
|
44
backend/src/interfaces/question.ts
Normal file
44
backend/src/interfaces/question.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Question } from '../entities/questions/question.entity.js';
|
||||||
|
import { UserDTO } from './user.js';
|
||||||
|
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||||
|
import { mapToStudentDTO, StudentDTO } from './student.js';
|
||||||
|
import { TeacherDTO } from './teacher.js';
|
||||||
|
|
||||||
|
export interface QuestionDTO {
|
||||||
|
learningObjectIdentifier: LearningObjectIdentifier;
|
||||||
|
sequenceNumber?: number;
|
||||||
|
author: StudentDTO;
|
||||||
|
timestamp?: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
learningObjectIdentifier,
|
||||||
|
sequenceNumber: question.sequenceNumber!,
|
||||||
|
author: mapToStudentDTO(question.author),
|
||||||
|
timestamp: question.timestamp.toISOString(),
|
||||||
|
content: question.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionId {
|
||||||
|
learningObjectIdentifier: LearningObjectIdentifier;
|
||||||
|
sequenceNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToQuestionId(question: QuestionDTO): QuestionId {
|
||||||
|
return {
|
||||||
|
learningObjectIdentifier: question.learningObjectIdentifier,
|
||||||
|
sequenceNumber: question.sequenceNumber!,
|
||||||
|
};
|
||||||
|
}
|
29
backend/src/interfaces/student.ts
Normal file
29
backend/src/interfaces/student.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToStudentDTO(student: Student): StudentDTO {
|
||||||
|
return {
|
||||||
|
id: student.username,
|
||||||
|
username: student.username,
|
||||||
|
firstName: student.firstName,
|
||||||
|
lastName: student.lastName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToStudent(studentData: StudentDTO): Student {
|
||||||
|
const student = new Student(studentData.username, studentData.firstName, studentData.lastName);
|
||||||
|
|
||||||
|
return student;
|
||||||
|
}
|
47
backend/src/interfaces/submission.ts
Normal file
47
backend/src/interfaces/submission.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
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 { mapToUser } from './user';
|
||||||
|
import { Student } from '../entities/users/student.entity';
|
||||||
|
|
||||||
|
export interface SubmissionDTO {
|
||||||
|
learningObjectHruid: string;
|
||||||
|
learningObjectLanguage: Language;
|
||||||
|
learningObjectVersion: number;
|
||||||
|
|
||||||
|
submissionNumber?: number;
|
||||||
|
submitter: StudentDTO;
|
||||||
|
time?: Date;
|
||||||
|
group?: GroupDTO;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
||||||
|
return {
|
||||||
|
learningObjectHruid: submission.learningObjectHruid,
|
||||||
|
learningObjectLanguage: submission.learningObjectLanguage,
|
||||||
|
learningObjectVersion: submission.learningObjectVersion,
|
||||||
|
|
||||||
|
submissionNumber: submission.submissionNumber,
|
||||||
|
submitter: mapToStudentDTO(submission.submitter),
|
||||||
|
time: submission.submissionTime,
|
||||||
|
group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf) : undefined,
|
||||||
|
content: submission.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
|
||||||
|
const submission = new Submission();
|
||||||
|
submission.learningObjectHruid = submissionDTO.learningObjectHruid;
|
||||||
|
submission.learningObjectLanguage = submissionDTO.learningObjectLanguage;
|
||||||
|
submission.learningObjectVersion = submissionDTO.learningObjectVersion;
|
||||||
|
// 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;
|
||||||
|
}
|
25
backend/src/interfaces/teacher-invitation.ts
Normal file
25
backend/src/interfaces/teacher-invitation.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO {
|
||||||
|
return {
|
||||||
|
sender: mapToUserDTO(invitation.sender),
|
||||||
|
receiver: mapToUserDTO(invitation.receiver),
|
||||||
|
class: mapToClassDTO(invitation.class),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): TeacherInvitationDTO {
|
||||||
|
return {
|
||||||
|
sender: invitation.sender.username,
|
||||||
|
receiver: invitation.receiver.username,
|
||||||
|
class: invitation.class.classId!,
|
||||||
|
};
|
||||||
|
}
|
29
backend/src/interfaces/teacher.ts
Normal file
29
backend/src/interfaces/teacher.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
|
||||||
|
return {
|
||||||
|
id: teacher.username,
|
||||||
|
username: teacher.username,
|
||||||
|
firstName: teacher.firstName,
|
||||||
|
lastName: teacher.lastName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToTeacher(TeacherData: TeacherDTO): Teacher {
|
||||||
|
const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName);
|
||||||
|
|
||||||
|
return teacher;
|
||||||
|
}
|
30
backend/src/interfaces/user.ts
Normal file
30
backend/src/interfaces/user.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToUserDTO(user: User): UserDTO {
|
||||||
|
return {
|
||||||
|
id: user.username,
|
||||||
|
username: user.username,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToUser<T extends User>(userData: UserDTO, userInstance: T): T {
|
||||||
|
userInstance.username = userData.username;
|
||||||
|
userInstance.firstName = userData.firstName;
|
||||||
|
userInstance.lastName = userData.lastName;
|
||||||
|
return userInstance;
|
||||||
|
}
|
|
@ -1,45 +0,0 @@
|
||||||
import express from 'express';
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
assignments: ['0', '1'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Information about an assignment with id 'id'
|
|
||||||
router.get('/:id', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
id: req.params.id,
|
|
||||||
title: 'Dit is een test assignment',
|
|
||||||
description: 'Een korte beschrijving',
|
|
||||||
groups: ['0'],
|
|
||||||
learningPath: '0',
|
|
||||||
class: '0',
|
|
||||||
links: {
|
|
||||||
self: `${req.baseUrl}/${req.params.id}`,
|
|
||||||
submissions: `${req.baseUrl}/${req.params.id}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id/submissions', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
submissions: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id/groups', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
groups: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id/questions', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
questions: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
30
backend/src/routes/assignments.ts
Normal file
30
backend/src/routes/assignments.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import express from 'express';
|
||||||
|
import {
|
||||||
|
createAssignmentHandler,
|
||||||
|
getAllAssignmentsHandler,
|
||||||
|
getAssignmentHandler,
|
||||||
|
getAssignmentsSubmissionsHandler,
|
||||||
|
} 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.get('/:id/submissions', getAssignmentsSubmissionsHandler);
|
||||||
|
|
||||||
|
router.get('/:id/questions', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
questions: ['0'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use('/:assignmentid/groups', groupRouter);
|
||||||
|
|
||||||
|
export default router;
|
|
@ -9,14 +9,17 @@ router.get('/config', (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
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!' });
|
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!' });
|
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!' });
|
res.json({ message: 'If you see this, you should be a teacher!' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
import express from 'express';
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
classes: ['0', '1'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Information about an class with id 'id'
|
|
||||||
router.get('/:id', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
id: req.params.id,
|
|
||||||
displayName: 'Klas 4B',
|
|
||||||
teachers: ['0'],
|
|
||||||
students: ['0'],
|
|
||||||
joinRequests: ['0'],
|
|
||||||
links: {
|
|
||||||
self: `${req.baseUrl}/${req.params.id}`,
|
|
||||||
classes: `${req.baseUrl}/${req.params.id}/invitations`,
|
|
||||||
questions: `${req.baseUrl}/${req.params.id}/assignments`,
|
|
||||||
students: `${req.baseUrl}/${req.params.id}/students`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id/invitations', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
invitations: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id/assignments', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
assignments: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id/students', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
students: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
27
backend/src/routes/classes.ts
Normal file
27
backend/src/routes/classes.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import express from 'express';
|
||||||
|
import {
|
||||||
|
createClassHandler,
|
||||||
|
getAllClassesHandler,
|
||||||
|
getClassHandler,
|
||||||
|
getClassStudentsHandler,
|
||||||
|
getTeacherInvitationsHandler,
|
||||||
|
} from '../controllers/classes.js';
|
||||||
|
import assignmentRouter from './assignments.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Root endpoint used to search objects
|
||||||
|
router.get('/', getAllClassesHandler);
|
||||||
|
|
||||||
|
router.post('/', createClassHandler);
|
||||||
|
|
||||||
|
// Information about an class with id 'id'
|
||||||
|
router.get('/:id', getClassHandler);
|
||||||
|
|
||||||
|
router.get('/:id/teacher-invitations', getTeacherInvitationsHandler);
|
||||||
|
|
||||||
|
router.get('/:id/students', getClassStudentsHandler);
|
||||||
|
|
||||||
|
router.use('/:classid/assignments', assignmentRouter);
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,31 +0,0 @@
|
||||||
import express from 'express';
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
groups: ['0', '1'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Information about a group (members, ... [TODO DOC])
|
|
||||||
router.get('/:id', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
id: req.params.id,
|
|
||||||
assignment: '0',
|
|
||||||
students: ['0'],
|
|
||||||
submissions: ['0'],
|
|
||||||
// Reference to other endpoint
|
|
||||||
// Should be less hardcoded
|
|
||||||
questions: `/group/${req.params.id}/question`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// The list of questions a group has made
|
|
||||||
router.get('/:id/question', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
questions: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
23
backend/src/routes/groups.ts
Normal file
23
backend/src/routes/groups.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js';
|
||||||
|
|
||||||
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
|
// Root endpoint used to search objects
|
||||||
|
router.get('/', getAllGroupsHandler);
|
||||||
|
|
||||||
|
router.post('/', createGroupHandler);
|
||||||
|
|
||||||
|
// Information about a group (members, ... [TODO DOC])
|
||||||
|
router.get('/:groupid', getGroupHandler);
|
||||||
|
|
||||||
|
router.get('/:groupid', getGroupSubmissionsHandler);
|
||||||
|
|
||||||
|
// The list of questions a group has made
|
||||||
|
router.get('/:id/questions', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
questions: ['0'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,6 +1,9 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js';
|
import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js';
|
||||||
|
|
||||||
|
import submissionRoutes from './submissions.js';
|
||||||
|
import questionRoutes from './questions.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// DWENGO learning objects
|
// DWENGO learning objects
|
||||||
|
@ -21,6 +24,10 @@ router.get('/', getAllLearningObjects);
|
||||||
// Example: http://localhost:3000/learningObject/un_ai7
|
// Example: http://localhost:3000/learningObject/un_ai7
|
||||||
router.get('/:hruid', getLearningObject);
|
router.get('/:hruid', getLearningObject);
|
||||||
|
|
||||||
|
router.use('/:hruid/submissions', submissionRoutes);
|
||||||
|
|
||||||
|
router.use('/:hruid/:version/questions', questionRoutes);
|
||||||
|
|
||||||
// Parameter: hruid of learning object
|
// Parameter: hruid of learning object
|
||||||
// Query: language, version (optional)
|
// Query: language, version (optional)
|
||||||
// Route to fetch the HTML rendering of one learning object based on its hruid.
|
// Route to fetch the HTML rendering of one learning object based on its hruid.
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import express from 'express';
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
questions: ['0', '1'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Information about an question with id 'id'
|
|
||||||
router.get('/:id', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
id: req.params.id,
|
|
||||||
student: '0',
|
|
||||||
group: '0',
|
|
||||||
time: new Date(2025, 1, 1),
|
|
||||||
content: 'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????',
|
|
||||||
learningObject: '0',
|
|
||||||
links: {
|
|
||||||
self: `${req.baseUrl}/${req.params.id}`,
|
|
||||||
answers: `${req.baseUrl}/${req.params.id}/answers`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id/answers', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
answers: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
25
backend/src/routes/questions.ts
Normal file
25
backend/src/routes/questions.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import express from 'express';
|
||||||
|
import {
|
||||||
|
createQuestionHandler,
|
||||||
|
deleteQuestionHandler,
|
||||||
|
getAllQuestionsHandler,
|
||||||
|
getQuestionAnswersHandler,
|
||||||
|
getQuestionHandler,
|
||||||
|
} from '../controllers/questions.js';
|
||||||
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
|
// Query language
|
||||||
|
|
||||||
|
// Root endpoint used to search objects
|
||||||
|
router.get('/', getAllQuestionsHandler);
|
||||||
|
|
||||||
|
router.post('/', createQuestionHandler);
|
||||||
|
|
||||||
|
router.delete('/:seq', deleteQuestionHandler);
|
||||||
|
|
||||||
|
// Information about a question with id
|
||||||
|
router.get('/:seq', getQuestionHandler);
|
||||||
|
|
||||||
|
router.get('/answers/:seq', getQuestionAnswersHandler);
|
||||||
|
|
||||||
|
export default router;
|
35
backend/src/routes/router.ts
Normal file
35
backend/src/routes/router.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { Response, Router } from 'express';
|
||||||
|
import studentRouter from './students.js';
|
||||||
|
import groupRouter from './groups.js';
|
||||||
|
import assignmentRouter from './assignments.js';
|
||||||
|
import submissionRouter from './submissions.js';
|
||||||
|
import classRouter from './classes.js';
|
||||||
|
import questionRouter from './questions.js';
|
||||||
|
import authRouter from './auth.js';
|
||||||
|
import themeRoutes from './themes.js';
|
||||||
|
import learningPathRoutes from './learning-paths.js';
|
||||||
|
import learningObjectRoutes from './learning-objects.js';
|
||||||
|
import { getLogger, Logger } from '../logging/initalize.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
|
router.get('/', (_, res: Response) => {
|
||||||
|
logger.debug('GET /');
|
||||||
|
res.json({
|
||||||
|
message: 'Hello Dwengo!🚀',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
|
||||||
|
router.use('/group', groupRouter /* #swagger.tags = ['Group'] */);
|
||||||
|
router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */);
|
||||||
|
router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */);
|
||||||
|
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
|
||||||
|
router.use('/question', questionRouter /* #swagger.tags = ['Question'] */);
|
||||||
|
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
|
||||||
|
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
|
||||||
|
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);
|
||||||
|
router.use('/learningObject', learningObjectRoutes /* #swagger.tags = ['Learning Object'] */);
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,55 +0,0 @@
|
||||||
import express from 'express';
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
students: ['0', '1'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Information about a student's profile
|
|
||||||
router.get('/:id', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
id: req.params.id,
|
|
||||||
firstName: 'Jimmy',
|
|
||||||
lastName: 'Faster',
|
|
||||||
username: 'JimmyFaster2',
|
|
||||||
endpoints: {
|
|
||||||
classes: `/student/${req.params.id}/classes`,
|
|
||||||
questions: `/student/${req.params.id}/submissions`,
|
|
||||||
invitations: `/student/${req.params.id}/assignments`,
|
|
||||||
groups: `/student/${req.params.id}/groups`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// The list of classes a student is in
|
|
||||||
router.get('/:id/classes', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
classes: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// The list of submissions a student has made
|
|
||||||
router.get('/:id/submissions', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
submissions: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// The list of assignments a student has
|
|
||||||
router.get('/:id/assignments', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
assignments: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// The list of groups a student is in
|
|
||||||
router.get('/:id/groups', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
groups: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
46
backend/src/routes/students.ts
Normal file
46
backend/src/routes/students.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import express from 'express';
|
||||||
|
import {
|
||||||
|
createStudentHandler,
|
||||||
|
deleteStudentHandler,
|
||||||
|
getAllStudentsHandler,
|
||||||
|
getStudentAssignmentsHandler,
|
||||||
|
getStudentClassesHandler,
|
||||||
|
getStudentGroupsHandler,
|
||||||
|
getStudentHandler,
|
||||||
|
getStudentSubmissionsHandler,
|
||||||
|
} from '../controllers/students.js';
|
||||||
|
import { getStudentGroups } from '../services/students.js';
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Root endpoint used to search objects
|
||||||
|
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);
|
||||||
|
|
||||||
|
// The list of submissions a student has made
|
||||||
|
router.get('/:id/submissions', getStudentSubmissionsHandler);
|
||||||
|
|
||||||
|
// The list of assignments a student has
|
||||||
|
router.get('/:id/assignments', getStudentAssignmentsHandler);
|
||||||
|
|
||||||
|
// The list of groups a student is in
|
||||||
|
router.get('/:id/groups', getStudentGroupsHandler);
|
||||||
|
|
||||||
|
// A list of questions a user has created
|
||||||
|
router.get('/:id/questions', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
questions: ['0'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,23 +0,0 @@
|
||||||
import express from 'express';
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
submissions: ['0', '1'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Information about an submission with id 'id'
|
|
||||||
router.get('/:id', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
id: req.params.id,
|
|
||||||
student: '0',
|
|
||||||
group: '0',
|
|
||||||
time: new Date(2025, 1, 1),
|
|
||||||
content: 'Wortel 2 is rationeel',
|
|
||||||
learningObject: '0',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
19
backend/src/routes/submissions.ts
Normal file
19
backend/src/routes/submissions.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler } 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.post('/:id', createSubmissionHandler);
|
||||||
|
|
||||||
|
// Information about an submission with id 'id'
|
||||||
|
router.get('/:id', getSubmissionHandler);
|
||||||
|
|
||||||
|
router.delete('/:id', deleteSubmissionHandler);
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,48 +0,0 @@
|
||||||
import express from 'express';
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
teachers: ['0', '1'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Information about a teacher
|
|
||||||
router.get('/:id', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
id: req.params.id,
|
|
||||||
firstName: 'John',
|
|
||||||
lastName: 'Doe',
|
|
||||||
username: 'JohnDoe1',
|
|
||||||
links: {
|
|
||||||
self: `${req.baseUrl}/${req.params.id}`,
|
|
||||||
classes: `${req.baseUrl}/${req.params.id}/classes`,
|
|
||||||
questions: `${req.baseUrl}/${req.params.id}/questions`,
|
|
||||||
invitations: `${req.baseUrl}/${req.params.id}/invitations`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// The questions students asked a teacher
|
|
||||||
router.get('/:id/questions', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
questions: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invitations to other classes a teacher received
|
|
||||||
router.get('/:id/invitations', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
invitations: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// A list with ids of classes a teacher is in
|
|
||||||
router.get('/:id/classes', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
classes: ['0'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
37
backend/src/routes/teachers.ts
Normal file
37
backend/src/routes/teachers.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import express from 'express';
|
||||||
|
import {
|
||||||
|
createTeacherHandler,
|
||||||
|
deleteTeacherHandler,
|
||||||
|
getAllTeachersHandler,
|
||||||
|
getTeacherClassHandler,
|
||||||
|
getTeacherHandler,
|
||||||
|
getTeacherQuestionHandler,
|
||||||
|
getTeacherStudentHandler,
|
||||||
|
} from '../controllers/teachers.js';
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Root endpoint used to search objects
|
||||||
|
router.get('/', getAllTeachersHandler);
|
||||||
|
|
||||||
|
router.post('/', createTeacherHandler);
|
||||||
|
|
||||||
|
router.delete('/', deleteTeacherHandler);
|
||||||
|
|
||||||
|
router.get('/:username', getTeacherHandler);
|
||||||
|
|
||||||
|
router.delete('/:username', deleteTeacherHandler);
|
||||||
|
|
||||||
|
router.get('/:username/classes', getTeacherClassHandler);
|
||||||
|
|
||||||
|
router.get('/:username/students', getTeacherStudentHandler);
|
||||||
|
|
||||||
|
router.get('/:username/questions', getTeacherQuestionHandler);
|
||||||
|
|
||||||
|
// Invitations to other classes a teacher received
|
||||||
|
router.get('/:id/invitations', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
invitations: ['0'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
85
backend/src/services/assignments.ts
Normal file
85
backend/src/services/assignments.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
|
||||||
|
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
||||||
|
import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
|
||||||
|
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
|
||||||
|
|
||||||
|
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classid);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignmentRepository = getAssignmentRepository();
|
||||||
|
const assignments = await assignmentRepository.findAllAssignmentsInClass(cls);
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return assignments.map(mapToAssignmentDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return assignments.map(mapToAssignmentDTOId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<Assignment | null> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classid);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignment = mapToAssignment(assignmentData, cls);
|
||||||
|
const assignmentRepository = getAssignmentRepository();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newAssignment = assignmentRepository.create(assignment);
|
||||||
|
await assignmentRepository.save(newAssignment);
|
||||||
|
|
||||||
|
return newAssignment;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAssignment(classid: string, id: number): Promise<AssignmentDTO | null> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classid);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignmentRepository = getAssignmentRepository();
|
||||||
|
const assignment = await assignmentRepository.findByClassAndId(cls, id);
|
||||||
|
|
||||||
|
if (!assignment) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapToAssignmentDTO(assignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAssignmentsSubmissions(classid: string, assignmentNumber: number): Promise<SubmissionDTO[]> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classid);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignmentRepository = getAssignmentRepository();
|
||||||
|
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
|
||||||
|
|
||||||
|
if (!assignment) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupRepository = getGroupRepository();
|
||||||
|
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
|
||||||
|
|
||||||
|
const submissionRepository = getSubmissionRepository();
|
||||||
|
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
|
||||||
|
|
||||||
|
return submissions.map(mapToSubmissionDTO);
|
||||||
|
}
|
99
backend/src/services/class.ts
Normal file
99
backend/src/services/class.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js';
|
||||||
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
|
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
||||||
|
import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
|
||||||
|
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js';
|
||||||
|
import { getLogger } from '../logging/initalize.js';
|
||||||
|
|
||||||
|
const logger = getLogger();
|
||||||
|
|
||||||
|
export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[]> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const classes = await classRepository.find({}, { populate: ['students', 'teachers'] });
|
||||||
|
|
||||||
|
if (!classes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return classes.map(mapToClassDTO);
|
||||||
|
}
|
||||||
|
return classes.map((cls) => cls.classId!);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createClass(classData: ClassDTO): Promise<Class | null> {
|
||||||
|
const teacherRepository = getTeacherRepository();
|
||||||
|
const teacherUsernames = classData.teachers || [];
|
||||||
|
const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher != null);
|
||||||
|
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
const studentUsernames = classData.students || [];
|
||||||
|
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null);
|
||||||
|
|
||||||
|
//Const cls = mapToClass(classData, teachers, students);
|
||||||
|
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newClass = classRepository.create({
|
||||||
|
displayName: classData.displayName,
|
||||||
|
teachers: teachers,
|
||||||
|
students: students,
|
||||||
|
});
|
||||||
|
await classRepository.save(newClass);
|
||||||
|
|
||||||
|
return newClass;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClass(classId: string): Promise<ClassDTO | null> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classId);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapToClassDTO(cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchClassStudents(classId: string): Promise<StudentDTO[]> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classId);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return cls.students.map(mapToStudentDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClassStudents(classId: string): Promise<StudentDTO[]> {
|
||||||
|
return await fetchClassStudents(classId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClassStudentsIds(classId: string): Promise<string[]> {
|
||||||
|
const students: StudentDTO[] = await fetchClassStudents(classId);
|
||||||
|
return students.map((student) => student.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClassTeacherInvitations(classId: string, full: boolean): Promise<TeacherInvitationDTO[]> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classId);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const teacherInvitationRepository = getTeacherInvitationRepository();
|
||||||
|
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(cls);
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return invitations.map(mapToTeacherInvitationDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return invitations.map(mapToTeacherInvitationDTOIds);
|
||||||
|
}
|
132
backend/src/services/groups.ts
Normal file
132
backend/src/services/groups.ts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import { GroupRepository } from '../data/assignments/group-repository.js';
|
||||||
|
import {
|
||||||
|
getAssignmentRepository,
|
||||||
|
getClassRepository,
|
||||||
|
getGroupRepository,
|
||||||
|
getStudentRepository,
|
||||||
|
getSubmissionRepository,
|
||||||
|
} from '../data/repositories.js';
|
||||||
|
import { Group } from '../entities/assignments/group.entity.js';
|
||||||
|
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
||||||
|
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
|
||||||
|
|
||||||
|
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classId);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignmentRepository = getAssignmentRepository();
|
||||||
|
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
|
||||||
|
|
||||||
|
if (!assignment) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupRepository = getGroupRepository();
|
||||||
|
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return mapToGroupDTO(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapToGroupDTOId(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise<Group | null> {
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
|
||||||
|
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
|
||||||
|
const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null);
|
||||||
|
|
||||||
|
console.log(members);
|
||||||
|
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classid);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignmentRepository = getAssignmentRepository();
|
||||||
|
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
|
||||||
|
|
||||||
|
if (!assignment) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupRepository = getGroupRepository();
|
||||||
|
try {
|
||||||
|
const newGroup = groupRepository.create({
|
||||||
|
assignment: assignment,
|
||||||
|
members: members,
|
||||||
|
});
|
||||||
|
await groupRepository.save(newGroup);
|
||||||
|
|
||||||
|
return newGroup;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllGroups(classId: string, assignmentNumber: number, full: boolean): Promise<GroupDTO[]> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classId);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignmentRepository = getAssignmentRepository();
|
||||||
|
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
|
||||||
|
|
||||||
|
if (!assignment) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupRepository = getGroupRepository();
|
||||||
|
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
console.log('full');
|
||||||
|
console.log(groups);
|
||||||
|
return groups.map(mapToGroupDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.map(mapToGroupDTOId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise<SubmissionDTO[]> {
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const cls = await classRepository.findById(classId);
|
||||||
|
|
||||||
|
if (!cls) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignmentRepository = getAssignmentRepository();
|
||||||
|
const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber);
|
||||||
|
|
||||||
|
if (!assignment) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupRepository = getGroupRepository();
|
||||||
|
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const submissionRepository = getSubmissionRepository();
|
||||||
|
const submissions = await submissionRepository.findAllSubmissionsForGroup(group);
|
||||||
|
|
||||||
|
return submissions.map(mapToSubmissionDTO);
|
||||||
|
}
|
90
backend/src/services/learning-objects.ts
Normal file
90
backend/src/services/learning-objects.ts
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { DWENGO_API_BASE } from '../config.js';
|
||||||
|
import { fetchWithLogging } from '../util/api-helper.js';
|
||||||
|
import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js';
|
||||||
|
|
||||||
|
function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
|
||||||
|
return {
|
||||||
|
key: data.hruid, // Hruid learningObject (not path)
|
||||||
|
_id: data._id,
|
||||||
|
uuid: data.uuid,
|
||||||
|
version: data.version,
|
||||||
|
title: data.title,
|
||||||
|
htmlUrl, // Url to fetch html content
|
||||||
|
language: data.language,
|
||||||
|
difficulty: data.difficulty,
|
||||||
|
estimatedTime: data.estimated_time,
|
||||||
|
available: data.available,
|
||||||
|
teacherExclusive: data.teacher_exclusive,
|
||||||
|
educationalGoals: data.educational_goals, // List with learningObjects
|
||||||
|
keywords: data.keywords, // For search
|
||||||
|
description: data.description, // For search (not an actual description)
|
||||||
|
targetAges: data.target_ages,
|
||||||
|
contentType: data.content_type, // Markdown, image, audio, etc.
|
||||||
|
contentLocation: data.content_location, // If content type extern
|
||||||
|
skosConcepts: data.skos_concepts,
|
||||||
|
returnValue: data.return_value, // Callback response information
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a single learning object by its HRUID
|
||||||
|
*/
|
||||||
|
export async function getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> {
|
||||||
|
const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`;
|
||||||
|
const metadata = await fetchWithLogging<LearningObjectMetadata>(
|
||||||
|
metadataUrl,
|
||||||
|
`Metadata for Learning Object HRUID "${hruid}" (language ${language})`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!metadata) {
|
||||||
|
console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`;
|
||||||
|
return filterData(metadata, htmlUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic function to fetch learning objects (full data or just HRUIDs)
|
||||||
|
*/
|
||||||
|
async function fetchLearningObjects(hruid: string, full: boolean, language: string): Promise<FilteredLearningObject[] | string[]> {
|
||||||
|
try {
|
||||||
|
const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`);
|
||||||
|
|
||||||
|
if (!learningPathResponse.success || !learningPathResponse.data?.length) {
|
||||||
|
console.error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes: LearningObjectNode[] = learningPathResponse.data[0].nodes;
|
||||||
|
|
||||||
|
if (!full) {
|
||||||
|
return nodes.map((node) => node.learningobject_hruid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Promise.all(nodes.map(async (node) => getLearningObjectById(node.learningobject_hruid, language))).then((objects) =>
|
||||||
|
objects.filter((obj): obj is FilteredLearningObject => obj !== null)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error fetching learning objects:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch full learning object data (metadata)
|
||||||
|
*/
|
||||||
|
export async function getLearningObjectsFromPath(hruid: string, language: string): Promise<FilteredLearningObject[]> {
|
||||||
|
return (await fetchLearningObjects(hruid, true, language)) as FilteredLearningObject[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch only learning object HRUIDs
|
||||||
|
*/
|
||||||
|
export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> {
|
||||||
|
return (await fetchLearningObjects(hruid, false, language)) as string[];
|
||||||
|
}
|
||||||
|
function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { DWENGO_API_BASE } from '../../config.js';
|
import { DWENGO_API_BASE } from '../../config.js';
|
||||||
import { fetchWithLogging } from '../../util/apiHelper.js';
|
import { fetchWithLogging } from '../../util/api-helper.js';
|
||||||
import {
|
import {
|
||||||
FilteredLearningObject,
|
FilteredLearningObject,
|
||||||
LearningObjectIdentifier,
|
LearningObjectIdentifier,
|
||||||
|
|
|
@ -29,7 +29,7 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (nullableNodesToLearningObjects.values().some((it) => it === null)) {
|
if (Array.from(nullableNodesToLearningObjects.values()).some((it) => it === null)) {
|
||||||
throw new Error('At least one of the learning objects on this path could not be found.');
|
throw new Error('At least one of the learning objects on this path could not be found.');
|
||||||
}
|
}
|
||||||
return nullableNodesToLearningObjects as Map<LearningPathNode, FilteredLearningObject>;
|
return nullableNodesToLearningObjects as Map<LearningPathNode, FilteredLearningObject>;
|
||||||
|
@ -41,15 +41,9 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
|
||||||
async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: PersonalizationTarget): Promise<LearningPath> {
|
async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: PersonalizationTarget): Promise<LearningPath> {
|
||||||
const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes);
|
const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes);
|
||||||
|
|
||||||
const targetAges = nodesToLearningObjects
|
const targetAges = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.targetAges || []);
|
||||||
.values()
|
|
||||||
.flatMap((it) => it.targetAges || [])
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
const keywords = nodesToLearningObjects
|
const keywords = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.keywords || []);
|
||||||
.values()
|
|
||||||
.flatMap((it) => it.keywords || [])
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
const image = learningPath.image ? learningPath.image.toString('base64') : undefined;
|
const image = learningPath.image ? learningPath.image.toString('base64') : undefined;
|
||||||
|
|
||||||
|
@ -83,9 +77,7 @@ async function convertNodes(
|
||||||
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
|
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
|
||||||
personalizedFor?: PersonalizationTarget
|
personalizedFor?: PersonalizationTarget
|
||||||
): Promise<LearningObjectNode[]> {
|
): Promise<LearningObjectNode[]> {
|
||||||
const nodesPromise = nodesToLearningObjects
|
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => {
|
||||||
.entries()
|
|
||||||
.map(async (entry) => {
|
|
||||||
const [node, learningObject] = entry;
|
const [node, learningObject] = entry;
|
||||||
const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null;
|
const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null;
|
||||||
return {
|
return {
|
||||||
|
@ -102,8 +94,7 @@ async function convertNodes(
|
||||||
)
|
)
|
||||||
.map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition
|
.map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
.toArray();
|
|
||||||
return await Promise.all(nodesPromise);
|
return await Promise.all(nodesPromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { fetchWithLogging } from '../../util/apiHelper.js';
|
import { fetchWithLogging } from '../../util/api-helper.js';
|
||||||
import { DWENGO_API_BASE } from '../../config.js';
|
import { DWENGO_API_BASE } from '../../config.js';
|
||||||
import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js';
|
import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js';
|
||||||
import { LearningPathProvider } from './learning-path-provider.js';
|
import { LearningPathProvider } from './learning-path-provider.js';
|
||||||
|
|
107
backend/src/services/questions.ts
Normal file
107
backend/src/services/questions.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js';
|
||||||
|
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
|
||||||
|
import { Question } from '../entities/questions/question.entity.js';
|
||||||
|
import { Answer } from '../entities/questions/answer.entity.js';
|
||||||
|
import { mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js';
|
||||||
|
import { QuestionRepository } from '../data/questions/question-repository.js';
|
||||||
|
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||||
|
import { mapToUser } from '../interfaces/user.js';
|
||||||
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
|
import { mapToStudent } from '../interfaces/student.js';
|
||||||
|
|
||||||
|
export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
|
||||||
|
const questionRepository: QuestionRepository = getQuestionRepository();
|
||||||
|
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
||||||
|
|
||||||
|
if (!questions) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const questionsDTO: QuestionDTO[] = questions.map(mapToQuestionDTO);
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return questionsDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return questionsDTO.map(mapToQuestionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchQuestion(questionId: QuestionId): Promise<Question | null> {
|
||||||
|
const questionRepository = getQuestionRepository();
|
||||||
|
|
||||||
|
return await questionRepository.findOne({
|
||||||
|
learningObjectHruid: questionId.learningObjectIdentifier.hruid,
|
||||||
|
learningObjectLanguage: questionId.learningObjectIdentifier.language,
|
||||||
|
learningObjectVersion: questionId.learningObjectIdentifier.version,
|
||||||
|
sequenceNumber: questionId.sequenceNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO | null> {
|
||||||
|
const question = await fetchQuestion(questionId);
|
||||||
|
|
||||||
|
if (!question) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapToQuestionDTO(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAnswersByQuestion(questionId: QuestionId, full: boolean) {
|
||||||
|
const answerRepository = getAnswerRepository();
|
||||||
|
const question = await fetchQuestion(questionId);
|
||||||
|
|
||||||
|
if (!question) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question);
|
||||||
|
|
||||||
|
if (!answers) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const answersDTO = answers.map(mapToAnswerDTO);
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return answersDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return answersDTO.map(mapToAnswerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createQuestion(questionDTO: QuestionDTO) {
|
||||||
|
const questionRepository = getQuestionRepository();
|
||||||
|
|
||||||
|
const author = mapToStudent(questionDTO.author);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await questionRepository.createQuestion({
|
||||||
|
loId: questionDTO.learningObjectIdentifier,
|
||||||
|
author,
|
||||||
|
content: questionDTO.content,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return questionDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteQuestion(questionId: QuestionId) {
|
||||||
|
const questionRepository = getQuestionRepository();
|
||||||
|
|
||||||
|
const question = await fetchQuestion(questionId);
|
||||||
|
|
||||||
|
if (!question) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(questionId.learningObjectIdentifier, questionId.sequenceNumber);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return question;
|
||||||
|
}
|
126
backend/src/services/students.ts
Normal file
126
backend/src/services/students.ts
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js';
|
||||||
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
|
import { AssignmentDTO } from '../interfaces/assignment.js';
|
||||||
|
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
||||||
|
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
||||||
|
import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
|
||||||
|
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
|
||||||
|
import { getAllAssignments } from './assignments.js';
|
||||||
|
import { UserService } from './users.js';
|
||||||
|
|
||||||
|
export async function getAllStudents(): Promise<StudentDTO[]> {
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
const users = await studentRepository.findAll();
|
||||||
|
return users.map(mapToStudentDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllStudentIds(): Promise<string[]> {
|
||||||
|
const users = await getAllStudents();
|
||||||
|
return users.map((user) => user.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudent(username: string): Promise<StudentDTO | null> {
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
const user = await studentRepository.findByUsername(username);
|
||||||
|
return user ? mapToStudentDTO(user) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> {
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newStudent = studentRepository.create(mapToStudent(userData));
|
||||||
|
await studentRepository.save(newStudent);
|
||||||
|
|
||||||
|
return mapToStudentDTO(newStudent);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteStudent(username: string): Promise<StudentDTO | null> {
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
|
||||||
|
const user = await studentRepository.findByUsername(username);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await studentRepository.deleteByUsername(username);
|
||||||
|
|
||||||
|
return mapToStudentDTO(user);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentClasses(username: string, full: boolean): Promise<ClassDTO[] | string[]> {
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
const student = await studentRepository.findByUsername(username);
|
||||||
|
|
||||||
|
if (!student) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const classes = await classRepository.findByStudent(student);
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return classes.map(mapToClassDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.map((cls) => cls.classId!);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentAssignments(username: string, full: boolean): Promise<AssignmentDTO[]> {
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
const student = await studentRepository.findByUsername(username);
|
||||||
|
|
||||||
|
if (!student) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const classes = await classRepository.findByStudent(student);
|
||||||
|
|
||||||
|
const assignments = (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat();
|
||||||
|
|
||||||
|
return assignments;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[]> {
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
const student = await studentRepository.findByUsername(username);
|
||||||
|
|
||||||
|
if (!student) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupRepository = getGroupRepository();
|
||||||
|
const groups = await groupRepository.findAllGroupsWithStudent(student);
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return groups.map(mapToGroupDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.map(mapToGroupDTOId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentSubmissions(username: string): Promise<SubmissionDTO[]> {
|
||||||
|
const studentRepository = getStudentRepository();
|
||||||
|
const student = await studentRepository.findByUsername(username);
|
||||||
|
|
||||||
|
if (!student) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const submissionRepository = getSubmissionRepository();
|
||||||
|
const submissions = await submissionRepository.findAllSubmissionsForStudent(student);
|
||||||
|
|
||||||
|
return submissions.map(mapToSubmissionDTO);
|
||||||
|
}
|
51
backend/src/services/submissions.ts
Normal file
51
backend/src/services/submissions.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
|
||||||
|
import { Language } from '../entities/content/language.js';
|
||||||
|
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||||
|
import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
|
||||||
|
|
||||||
|
export async function getSubmission(
|
||||||
|
learningObjectHruid: string,
|
||||||
|
language: Language,
|
||||||
|
version: number,
|
||||||
|
submissionNumber: number
|
||||||
|
): Promise<SubmissionDTO | null> {
|
||||||
|
const loId = new LearningObjectIdentifier(learningObjectHruid, language, version);
|
||||||
|
|
||||||
|
const submissionRepository = getSubmissionRepository();
|
||||||
|
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber);
|
||||||
|
|
||||||
|
if (!submission) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapToSubmissionDTO(submission);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSubmission(submissionDTO: SubmissionDTO) {
|
||||||
|
const submissionRepository = getSubmissionRepository();
|
||||||
|
const submission = mapToSubmission(submissionDTO);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newSubmission = await submissionRepository.create(submission);
|
||||||
|
await submissionRepository.save(newSubmission);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return submission;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) {
|
||||||
|
const submissionRepository = getSubmissionRepository();
|
||||||
|
|
||||||
|
const submission = getSubmission(learningObjectHruid, language, version, submissionNumber);
|
||||||
|
|
||||||
|
if (!submission) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loId = new LearningObjectIdentifier(learningObjectHruid, language, version);
|
||||||
|
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber);
|
||||||
|
|
||||||
|
return submission;
|
||||||
|
}
|
129
backend/src/services/teachers.ts
Normal file
129
backend/src/services/teachers.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import {
|
||||||
|
getClassRepository,
|
||||||
|
getLearningObjectRepository,
|
||||||
|
getQuestionRepository,
|
||||||
|
getStudentRepository,
|
||||||
|
getTeacherRepository,
|
||||||
|
} from '../data/repositories.js';
|
||||||
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
|
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
||||||
|
import { getClassStudents } from './class.js';
|
||||||
|
import { StudentDTO } from '../interfaces/student.js';
|
||||||
|
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
|
||||||
|
import { UserService } from './users.js';
|
||||||
|
import { mapToUser } from '../interfaces/user.js';
|
||||||
|
import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js';
|
||||||
|
|
||||||
|
export async function getAllTeachers(): Promise<TeacherDTO[]> {
|
||||||
|
const teacherRepository = getTeacherRepository();
|
||||||
|
const users = await teacherRepository.findAll();
|
||||||
|
return users.map(mapToTeacherDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllTeacherIds(): Promise<string[]> {
|
||||||
|
const users = await getAllTeachers();
|
||||||
|
return users.map((user) => user.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTeacher(username: string): Promise<TeacherDTO | null> {
|
||||||
|
const teacherRepository = getTeacherRepository();
|
||||||
|
const user = await teacherRepository.findByUsername(username);
|
||||||
|
return user ? mapToTeacherDTO(user) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> {
|
||||||
|
const teacherRepository = getTeacherRepository();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newTeacher = teacherRepository.create(mapToTeacher(userData));
|
||||||
|
await teacherRepository.save(newTeacher);
|
||||||
|
|
||||||
|
return mapToTeacherDTO(newTeacher);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTeacher(username: string): Promise<TeacherDTO | null> {
|
||||||
|
const teacherRepository = getTeacherRepository();
|
||||||
|
|
||||||
|
const user = await teacherRepository.findByUsername(username);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await teacherRepository.deleteByUsername(username);
|
||||||
|
|
||||||
|
return mapToTeacherDTO(user);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[]> {
|
||||||
|
const teacherRepository = getTeacherRepository();
|
||||||
|
const teacher = await teacherRepository.findByUsername(username);
|
||||||
|
if (!teacher) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const classRepository = getClassRepository();
|
||||||
|
const classes = await classRepository.findByTeacher(teacher);
|
||||||
|
return classes.map(mapToClassDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClassesByTeacher(username: string): Promise<ClassDTO[]> {
|
||||||
|
return await fetchClassesByTeacher(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClassIdsByTeacher(username: string): Promise<string[]> {
|
||||||
|
const classes = await fetchClassesByTeacher(username);
|
||||||
|
return classes.map((cls) => cls.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchStudentsByTeacher(username: string) {
|
||||||
|
const classes = await getClassIdsByTeacher(username);
|
||||||
|
|
||||||
|
return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentsByTeacher(username: string): Promise<StudentDTO[]> {
|
||||||
|
return await fetchStudentsByTeacher(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentIdsByTeacher(username: string): Promise<string[]> {
|
||||||
|
const students = await fetchStudentsByTeacher(username);
|
||||||
|
return students.map((student) => student.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[]> {
|
||||||
|
const teacherRepository = getTeacherRepository();
|
||||||
|
const teacher = await teacherRepository.findByUsername(username);
|
||||||
|
if (!teacher) {
|
||||||
|
throw new Error(`Teacher with username '${username}' not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all learning objects that this teacher manages
|
||||||
|
const learningObjectRepository = getLearningObjectRepository();
|
||||||
|
const learningObjects = await learningObjectRepository.findAllByTeacher(teacher);
|
||||||
|
|
||||||
|
// Fetch all questions related to these learning objects
|
||||||
|
const questionRepository = getQuestionRepository();
|
||||||
|
const questions = await questionRepository.findAllByLearningObjects(learningObjects);
|
||||||
|
|
||||||
|
return questions.map(mapToQuestionDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getQuestionsByTeacher(username: string): Promise<QuestionDTO[]> {
|
||||||
|
return await fetchTeacherQuestions(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getQuestionIdsByTeacher(username: string): Promise<QuestionId[]> {
|
||||||
|
const questions = await fetchTeacherQuestions(username);
|
||||||
|
|
||||||
|
return questions.map(mapToQuestionId);
|
||||||
|
}
|
41
backend/src/services/users.ts
Normal file
41
backend/src/services/users.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { UserRepository } from '../data/users/user-repository.js';
|
||||||
|
import { UserDTO, mapToUser, mapToUserDTO } from '../interfaces/user.js';
|
||||||
|
import { User } from '../entities/users/user.entity.js';
|
||||||
|
|
||||||
|
export class UserService<T extends User> {
|
||||||
|
protected repository: UserRepository<T>;
|
||||||
|
|
||||||
|
constructor(repository: UserRepository<T>) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllUsers(): Promise<UserDTO[]> {
|
||||||
|
const users = await this.repository.findAll();
|
||||||
|
return users.map(mapToUserDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllUserIds(): Promise<string[]> {
|
||||||
|
const users = await this.getAllUsers();
|
||||||
|
return users.map((user) => user.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserByUsername(username: string): Promise<UserDTO | null> {
|
||||||
|
const user = await this.repository.findByUsername(username);
|
||||||
|
return user ? mapToUserDTO(user) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(userData: UserDTO, UserClass: new () => T): Promise<T> {
|
||||||
|
const newUser = mapToUser(userData, new UserClass());
|
||||||
|
await this.repository.save(newUser);
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUser(username: string): Promise<UserDTO | null> {
|
||||||
|
const user = await this.getUserByUsername(username);
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await this.repository.deleteByUsername(username);
|
||||||
|
return mapToUserDTO(user);
|
||||||
|
}
|
||||||
|
}
|
7
backend/src/swagger.ts
Normal file
7
backend/src/swagger.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { RequestHandler } from 'express';
|
||||||
|
import swaggerUi from 'swagger-ui-express';
|
||||||
|
import swaggerDocument from '../../docs/api/swagger.json' with { type: 'json' };
|
||||||
|
|
||||||
|
const swaggerMiddleware: RequestHandler = swaggerUi.setup(swaggerDocument);
|
||||||
|
|
||||||
|
export default swaggerMiddleware;
|
|
@ -32,7 +32,7 @@ describe('SubmissionRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should find the requested submission', async () => {
|
it('should find the requested submission', async () => {
|
||||||
const id = new LearningObjectIdentifier('id03', Language.English, '1');
|
const id = new LearningObjectIdentifier('id03', Language.English, 1);
|
||||||
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
||||||
|
|
||||||
expect(submission).toBeTruthy();
|
expect(submission).toBeTruthy();
|
||||||
|
@ -40,7 +40,7 @@ describe('SubmissionRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should find the most recent submission for a student', async () => {
|
it('should find the most recent submission for a student', async () => {
|
||||||
const id = new LearningObjectIdentifier('id02', Language.English, '1');
|
const id = new LearningObjectIdentifier('id02', Language.English, 1);
|
||||||
const student = await studentRepository.findByUsername('Noordkaap');
|
const student = await studentRepository.findByUsername('Noordkaap');
|
||||||
const submission = await submissionRepository.findMostRecentSubmissionForStudent(id, student!);
|
const submission = await submissionRepository.findMostRecentSubmissionForStudent(id, student!);
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ describe('SubmissionRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should find the most recent submission for a group', async () => {
|
it('should find the most recent submission for a group', async () => {
|
||||||
const id = new LearningObjectIdentifier('id03', Language.English, '1');
|
const id = new LearningObjectIdentifier('id03', Language.English, 1);
|
||||||
const class_ = await classRepository.findById('id01');
|
const class_ = await classRepository.findById('id01');
|
||||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 1);
|
const assignment = await assignmentRepository.findByClassAndId(class_!, 1);
|
||||||
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1);
|
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1);
|
||||||
|
@ -60,7 +60,7 @@ describe('SubmissionRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not find a deleted submission', async () => {
|
it('should not find a deleted submission', async () => {
|
||||||
const id = new LearningObjectIdentifier('id01', Language.English, '1');
|
const id = new LearningObjectIdentifier('id01', Language.English, 1);
|
||||||
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
||||||
|
|
||||||
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
||||||
|
|
|
@ -17,11 +17,11 @@ describe('AttachmentRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the requested attachment', async () => {
|
it('should return the requested attachment', async () => {
|
||||||
const id = new LearningObjectIdentifier('id02', Language.English, '1');
|
const id = new LearningObjectIdentifier('id02', Language.English, 1);
|
||||||
const learningObject = await learningObjectRepository.findByIdentifier(id);
|
const learningObject = await learningObjectRepository.findByIdentifier(id);
|
||||||
|
|
||||||
const attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName(
|
const attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName(
|
||||||
learningObject!,
|
learningObject!.hruid,
|
||||||
Language.English,
|
Language.English,
|
||||||
'attachment01'
|
'attachment01'
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,8 +13,8 @@ describe('LearningObjectRepository', () => {
|
||||||
learningObjectRepository = getLearningObjectRepository();
|
learningObjectRepository = getLearningObjectRepository();
|
||||||
});
|
});
|
||||||
|
|
||||||
const id01 = new LearningObjectIdentifier('id01', Language.English, '1');
|
const id01 = new LearningObjectIdentifier('id01', Language.English, 1);
|
||||||
const id02 = new LearningObjectIdentifier('test_id', Language.English, '1');
|
const id02 = new LearningObjectIdentifier('test_id', Language.English, 1);
|
||||||
|
|
||||||
it('should return the learning object that matches identifier 1', async () => {
|
it('should return the learning object that matches identifier 1', async () => {
|
||||||
const learningObject = await learningObjectRepository.findByIdentifier(id01);
|
const learningObject = await learningObjectRepository.findByIdentifier(id01);
|
||||||
|
|
|
@ -20,7 +20,7 @@ describe('AnswerRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should find all answers to a question', async () => {
|
it('should find all answers to a question', async () => {
|
||||||
const id = new LearningObjectIdentifier('id05', Language.English, '1');
|
const id = new LearningObjectIdentifier('id05', Language.English, 1);
|
||||||
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
||||||
|
|
||||||
const question = questions.filter((it) => it.sequenceNumber == 2)[0];
|
const question = questions.filter((it) => it.sequenceNumber == 2)[0];
|
||||||
|
@ -35,7 +35,7 @@ describe('AnswerRepository', () => {
|
||||||
|
|
||||||
it('should create an answer to a question', async () => {
|
it('should create an answer to a question', async () => {
|
||||||
const teacher = await teacherRepository.findByUsername('FooFighters');
|
const teacher = await teacherRepository.findByUsername('FooFighters');
|
||||||
const id = new LearningObjectIdentifier('id05', Language.English, '1');
|
const id = new LearningObjectIdentifier('id05', Language.English, 1);
|
||||||
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
||||||
|
|
||||||
const question = questions[0];
|
const question = questions[0];
|
||||||
|
@ -54,7 +54,7 @@ describe('AnswerRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not find a removed answer', async () => {
|
it('should not find a removed answer', async () => {
|
||||||
const id = new LearningObjectIdentifier('id04', Language.English, '1');
|
const id = new LearningObjectIdentifier('id04', Language.English, 1);
|
||||||
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
||||||
|
|
||||||
await answerRepository.removeAnswerByQuestionAndSequenceNumber(questions[0], 1);
|
await answerRepository.removeAnswerByQuestionAndSequenceNumber(questions[0], 1);
|
||||||
|
|
|
@ -20,7 +20,7 @@ describe('QuestionRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return all questions part of the given learning object', async () => {
|
it('should return all questions part of the given learning object', async () => {
|
||||||
const id = new LearningObjectIdentifier('id05', Language.English, '1');
|
const id = new LearningObjectIdentifier('id05', Language.English, 1);
|
||||||
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
||||||
|
|
||||||
expect(questions).toBeTruthy();
|
expect(questions).toBeTruthy();
|
||||||
|
@ -28,7 +28,7 @@ describe('QuestionRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create new question', async () => {
|
it('should create new question', async () => {
|
||||||
const id = new LearningObjectIdentifier('id03', Language.English, '1');
|
const id = new LearningObjectIdentifier('id03', Language.English, 1);
|
||||||
const student = await studentRepository.findByUsername('Noordkaap');
|
const student = await studentRepository.findByUsername('Noordkaap');
|
||||||
await questionRepository.createQuestion({
|
await questionRepository.createQuestion({
|
||||||
loId: id,
|
loId: id,
|
||||||
|
@ -42,7 +42,7 @@ describe('QuestionRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not find removed question', async () => {
|
it('should not find removed question', async () => {
|
||||||
const id = new LearningObjectIdentifier('id04', Language.English, '1');
|
const id = new LearningObjectIdentifier('id04', Language.English, 1);
|
||||||
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(id, 1);
|
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(id, 1);
|
||||||
|
|
||||||
const question = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
const question = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
||||||
|
|
|
@ -12,7 +12,7 @@ export function makeTestSubmissions(
|
||||||
const submission01 = em.create(Submission, {
|
const submission01 = em.create(Submission, {
|
||||||
learningObjectHruid: 'id03',
|
learningObjectHruid: 'id03',
|
||||||
learningObjectLanguage: Language.English,
|
learningObjectLanguage: Language.English,
|
||||||
learningObjectVersion: '1',
|
learningObjectVersion: 1,
|
||||||
submissionNumber: 1,
|
submissionNumber: 1,
|
||||||
submitter: students[0],
|
submitter: students[0],
|
||||||
submissionTime: new Date(2025, 2, 20),
|
submissionTime: new Date(2025, 2, 20),
|
||||||
|
@ -23,7 +23,7 @@ export function makeTestSubmissions(
|
||||||
const submission02 = em.create(Submission, {
|
const submission02 = em.create(Submission, {
|
||||||
learningObjectHruid: 'id03',
|
learningObjectHruid: 'id03',
|
||||||
learningObjectLanguage: Language.English,
|
learningObjectLanguage: Language.English,
|
||||||
learningObjectVersion: '1',
|
learningObjectVersion: 1,
|
||||||
submissionNumber: 2,
|
submissionNumber: 2,
|
||||||
submitter: students[0],
|
submitter: students[0],
|
||||||
submissionTime: new Date(2025, 2, 25),
|
submissionTime: new Date(2025, 2, 25),
|
||||||
|
@ -34,7 +34,7 @@ export function makeTestSubmissions(
|
||||||
const submission03 = em.create(Submission, {
|
const submission03 = em.create(Submission, {
|
||||||
learningObjectHruid: 'id02',
|
learningObjectHruid: 'id02',
|
||||||
learningObjectLanguage: Language.English,
|
learningObjectLanguage: Language.English,
|
||||||
learningObjectVersion: '1',
|
learningObjectVersion: 1,
|
||||||
submissionNumber: 1,
|
submissionNumber: 1,
|
||||||
submitter: students[0],
|
submitter: students[0],
|
||||||
submissionTime: new Date(2025, 2, 20),
|
submissionTime: new Date(2025, 2, 20),
|
||||||
|
@ -44,7 +44,7 @@ export function makeTestSubmissions(
|
||||||
const submission04 = em.create(Submission, {
|
const submission04 = em.create(Submission, {
|
||||||
learningObjectHruid: 'id02',
|
learningObjectHruid: 'id02',
|
||||||
learningObjectLanguage: Language.English,
|
learningObjectLanguage: Language.English,
|
||||||
learningObjectVersion: '1',
|
learningObjectVersion: 1,
|
||||||
submissionNumber: 2,
|
submissionNumber: 2,
|
||||||
submitter: students[0],
|
submitter: students[0],
|
||||||
submissionTime: new Date(2025, 2, 25),
|
submissionTime: new Date(2025, 2, 25),
|
||||||
|
@ -54,7 +54,7 @@ export function makeTestSubmissions(
|
||||||
const submission05 = em.create(Submission, {
|
const submission05 = em.create(Submission, {
|
||||||
learningObjectHruid: 'id01',
|
learningObjectHruid: 'id01',
|
||||||
learningObjectLanguage: Language.English,
|
learningObjectLanguage: Language.English,
|
||||||
learningObjectVersion: '1',
|
learningObjectVersion: 1,
|
||||||
submissionNumber: 1,
|
submissionNumber: 1,
|
||||||
submitter: students[1],
|
submitter: students[1],
|
||||||
submissionTime: new Date(2025, 2, 20),
|
submissionTime: new Date(2025, 2, 20),
|
||||||
|
|
|
@ -77,7 +77,7 @@ export function makeTestLearningPaths(em: EntityManager<IDatabaseDriver<Connecti
|
||||||
admins: [],
|
admins: [],
|
||||||
title: 'repertoire Tool',
|
title: 'repertoire Tool',
|
||||||
description: 'all about Tool',
|
description: 'all about Tool',
|
||||||
image: '',
|
image: null,
|
||||||
nodes: nodes01,
|
nodes: nodes01,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ export function makeTestLearningPaths(em: EntityManager<IDatabaseDriver<Connecti
|
||||||
admins: [],
|
admins: [],
|
||||||
title: 'repertoire Dire Straits',
|
title: 'repertoire Dire Straits',
|
||||||
description: 'all about Dire Straits',
|
description: 'all about Dire Straits',
|
||||||
image: '',
|
image: null,
|
||||||
nodes: nodes02,
|
nodes: nodes02,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Student } from '../../../src/entities/users/student.entity';
|
||||||
export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>, students: Array<Student>): Array<Question> {
|
export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>, students: Array<Student>): Array<Question> {
|
||||||
const question01 = em.create(Question, {
|
const question01 = em.create(Question, {
|
||||||
learningObjectLanguage: Language.English,
|
learningObjectLanguage: Language.English,
|
||||||
learningObjectVersion: '1',
|
learningObjectVersion: 1,
|
||||||
learningObjectHruid: 'id05',
|
learningObjectHruid: 'id05',
|
||||||
sequenceNumber: 1,
|
sequenceNumber: 1,
|
||||||
author: students[0],
|
author: students[0],
|
||||||
|
@ -16,7 +16,7 @@ export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>
|
||||||
|
|
||||||
const question02 = em.create(Question, {
|
const question02 = em.create(Question, {
|
||||||
learningObjectLanguage: Language.English,
|
learningObjectLanguage: Language.English,
|
||||||
learningObjectVersion: '1',
|
learningObjectVersion: 1,
|
||||||
learningObjectHruid: 'id05',
|
learningObjectHruid: 'id05',
|
||||||
sequenceNumber: 2,
|
sequenceNumber: 2,
|
||||||
author: students[2],
|
author: students[2],
|
||||||
|
@ -26,7 +26,7 @@ export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>
|
||||||
|
|
||||||
const question03 = em.create(Question, {
|
const question03 = em.create(Question, {
|
||||||
learningObjectLanguage: Language.English,
|
learningObjectLanguage: Language.English,
|
||||||
learningObjectVersion: '1',
|
learningObjectVersion: 1,
|
||||||
learningObjectHruid: 'id04',
|
learningObjectHruid: 'id04',
|
||||||
sequenceNumber: 1,
|
sequenceNumber: 1,
|
||||||
author: students[0],
|
author: students[0],
|
||||||
|
@ -36,7 +36,7 @@ export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>
|
||||||
|
|
||||||
const question04 = em.create(Question, {
|
const question04 = em.create(Question, {
|
||||||
learningObjectLanguage: Language.English,
|
learningObjectLanguage: Language.English,
|
||||||
learningObjectVersion: '1',
|
learningObjectVersion: 1,
|
||||||
learningObjectHruid: 'id01',
|
learningObjectHruid: 'id01',
|
||||||
sequenceNumber: 1,
|
sequenceNumber: 1,
|
||||||
author: students[1],
|
author: students[1],
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"outDir": "./dist"
|
"outDir": "./dist",
|
||||||
|
"resolveJsonModule": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
72
compose.override.yml
Normal file
72
compose.override.yml
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
#
|
||||||
|
# Use this configuration to test the production configuration locally.
|
||||||
|
#
|
||||||
|
# This configuration builds the frontend and backend services as Docker images,
|
||||||
|
# and uses the paths for the services, instead of ports.
|
||||||
|
#
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: frontend/Dockerfile
|
||||||
|
ports:
|
||||||
|
- '8080:8080/tcp'
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- 'traefik.http.routers.web.rule=PathPrefix(`/`)'
|
||||||
|
- 'traefik.http.services.web.loadbalancer.server.port=8080'
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
ports:
|
||||||
|
- '3000:3000/tcp'
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./backend/.env:/app/.env
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- logging
|
||||||
|
labels:
|
||||||
|
- 'traefik.http.routers.api.rule=PathPrefix(`/api`)'
|
||||||
|
- 'traefik.http.services.api.loadbalancer.server.port=3000'
|
||||||
|
|
||||||
|
idp:
|
||||||
|
# Also see compose.yml
|
||||||
|
labels:
|
||||||
|
- 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)'
|
||||||
|
- 'traefik.http.services.idp.loadbalancer.server.port=7080'
|
||||||
|
environment:
|
||||||
|
PROXY_ADDRESS_FORWARDING: 'true'
|
||||||
|
KC_HTTP_RELATIVE_PATH: '/idp'
|
||||||
|
|
||||||
|
reverse-proxy:
|
||||||
|
image: traefik:v3.3
|
||||||
|
command:
|
||||||
|
# Enable web UI
|
||||||
|
- '--api.insecure=true'
|
||||||
|
|
||||||
|
# Add Docker provider
|
||||||
|
- '--providers.docker=true'
|
||||||
|
- '--providers.docker.exposedbydefault=true'
|
||||||
|
|
||||||
|
# Add web entrypoint
|
||||||
|
- '--entrypoints.web.address=:80/tcp'
|
||||||
|
ports:
|
||||||
|
- '9000:8080'
|
||||||
|
- '80:80/tcp'
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
|
||||||
|
dashboards:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
ports:
|
||||||
|
- '9002:3000'
|
||||||
|
volumes:
|
||||||
|
- dwengo_grafana_data:/var/lib/grafana
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dwengo_grafana_data:
|
112
compose.prod.yml
Normal file
112
compose.prod.yml
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
#
|
||||||
|
# This file is used to define the production environment for the project.
|
||||||
|
# It is used to deploy the project on a server.
|
||||||
|
# Should not be used for local development.
|
||||||
|
#
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: frontend/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dwengo-1
|
||||||
|
labels:
|
||||||
|
- 'traefik.enable=true'
|
||||||
|
- 'traefik.http.routers.web.rule=PathPrefix(`/`)'
|
||||||
|
- 'traefik.http.services.web.loadbalancer.server.port=8080'
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# TODO Replace with environment keys
|
||||||
|
- ./backend/.env:/app/.env
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- logging
|
||||||
|
networks:
|
||||||
|
- dwengo-1
|
||||||
|
labels:
|
||||||
|
- 'traefik.enable=true'
|
||||||
|
- 'traefik.http.routers.api.rule=PathPrefix(`/api`)'
|
||||||
|
- 'traefik.http.services.api.loadbalancer.server.port=3000'
|
||||||
|
|
||||||
|
db:
|
||||||
|
# Also see compose.yml
|
||||||
|
networks:
|
||||||
|
- dwengo-1
|
||||||
|
|
||||||
|
idp:
|
||||||
|
# Also see compose.yml
|
||||||
|
# TODO Replace with proper production command
|
||||||
|
command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm']
|
||||||
|
networks:
|
||||||
|
- dwengo-1
|
||||||
|
labels:
|
||||||
|
- 'traefik.enable=true'
|
||||||
|
- 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)'
|
||||||
|
- 'traefik.http.services.idp.loadbalancer.server.port=7080'
|
||||||
|
env_file:
|
||||||
|
- ./config/idp/.env
|
||||||
|
environment:
|
||||||
|
KC_HOSTNAME: 'sel2-1.ugent.be'
|
||||||
|
PROXY_ADDRESS_FORWARDING: 'true'
|
||||||
|
KC_PROXY_HEADERS: 'xforwarded'
|
||||||
|
KC_HTTP_ENABLED: 'true'
|
||||||
|
KC_HTTP_RELATIVE_PATH: '/idp'
|
||||||
|
|
||||||
|
reverse-proxy:
|
||||||
|
image: traefik:v3.3
|
||||||
|
ports:
|
||||||
|
- '80:80/tcp'
|
||||||
|
- '443:443/tcp'
|
||||||
|
command:
|
||||||
|
# Add Docker provider
|
||||||
|
- '--providers.docker=true'
|
||||||
|
- '--providers.docker.exposedbydefault=false'
|
||||||
|
|
||||||
|
# Add web entrypoint
|
||||||
|
- '--entrypoints.web.address=:80/tcp'
|
||||||
|
- '--entrypoints.web.http.redirections.entryPoint.to=websecure'
|
||||||
|
- '--entrypoints.web.http.redirections.entryPoint.scheme=https'
|
||||||
|
|
||||||
|
# Add websecure entrypoint
|
||||||
|
- '--entrypoints.websecure.address=:443/tcp'
|
||||||
|
- '--entrypoints.websecure.http.tls=true'
|
||||||
|
- '--entrypoints.websecure.http.tls.certResolver=letsencrypt'
|
||||||
|
- '--entrypoints.websecure.http.tls.domains[0].main=sel2-1.ugent.be'
|
||||||
|
|
||||||
|
# Certificates
|
||||||
|
- '--certificatesresolvers.letsencrypt.acme.httpchallenge=true'
|
||||||
|
- '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web'
|
||||||
|
- '--certificatesresolvers.letsencrypt.acme.email=timo.demeyst@ugent.be'
|
||||||
|
- '--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json'
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- dwengo_letsencrypt:/letsencrypt
|
||||||
|
networks:
|
||||||
|
- dwengo-1
|
||||||
|
|
||||||
|
logging:
|
||||||
|
# Also see compose.yml
|
||||||
|
networks:
|
||||||
|
- dwengo-1
|
||||||
|
|
||||||
|
dashboards:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
ports:
|
||||||
|
- '9002:3000'
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- dwengo_grafana_data:/var/lib/grafana
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dwengo_grafana_data:
|
||||||
|
dwengo_letsencrypt:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dwengo-1:
|
|
@ -1,38 +1,32 @@
|
||||||
|
#
|
||||||
|
# Use this configuration during development.
|
||||||
|
#
|
||||||
|
# This configuration is suitable to access the services using their ports.
|
||||||
|
#
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
|
ports:
|
||||||
|
- '5431:5432'
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- dwengo_postgres_data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: postgres
|
POSTGRES_DB: postgres
|
||||||
ports:
|
|
||||||
- '5431:5432'
|
|
||||||
volumes:
|
|
||||||
- dwengo_postgres_data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
logging:
|
|
||||||
image: grafana/loki:latest
|
|
||||||
ports:
|
|
||||||
- '3102:3102'
|
|
||||||
- '9095:9095'
|
|
||||||
volumes:
|
|
||||||
- ./config/loki/config.yml:/etc/loki/config.yaml
|
|
||||||
- dwengo_loki_data:/loki
|
|
||||||
command: -config.file=/etc/loki/config.yaml
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
dashboards:
|
|
||||||
image: grafana/grafana:latest
|
|
||||||
ports:
|
|
||||||
- '3100:3000'
|
|
||||||
volumes:
|
|
||||||
- dwengo_grafana_data:/var/lib/grafana
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
idp: # Based on: https://medium.com/@fingervinicius/easy-running-keycloak-with-docker-compose-b0d7a4ee2358
|
idp: # Based on: https://medium.com/@fingervinicius/easy-running-keycloak-with-docker-compose-b0d7a4ee2358
|
||||||
image: quay.io/keycloak/keycloak:latest
|
image: quay.io/keycloak/keycloak:latest
|
||||||
|
ports:
|
||||||
|
- '7080:7080'
|
||||||
|
# - '7443:7443'
|
||||||
|
command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm']
|
||||||
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./idp:/opt/keycloak/data/import
|
- ./config/idp:/opt/keycloak/data/import
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
environment:
|
environment:
|
||||||
KC_HOSTNAME: localhost
|
KC_HOSTNAME: localhost
|
||||||
KC_HOSTNAME_PORT: 7080
|
KC_HOSTNAME_PORT: 7080
|
||||||
|
@ -41,19 +35,18 @@ services:
|
||||||
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||||
KC_HEALTH_ENABLED: 'true'
|
KC_HEALTH_ENABLED: 'true'
|
||||||
KC_LOG_LEVEL: info
|
KC_LOG_LEVEL: info
|
||||||
healthcheck:
|
|
||||||
test: ['CMD', 'curl', '-f', 'http://localhost:7080/health/ready']
|
logging:
|
||||||
interval: 15s
|
image: grafana/loki:latest
|
||||||
timeout: 2s
|
|
||||||
retries: 15
|
|
||||||
command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm']
|
|
||||||
ports:
|
ports:
|
||||||
- '7080:7080'
|
- '9001:3102'
|
||||||
- '7443:7443'
|
- '9095:9095'
|
||||||
depends_on:
|
command: -config.file=/etc/loki/config.yaml
|
||||||
- db
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./config/loki/config.yml:/etc/loki/config.yaml
|
||||||
|
- dwengo_loki_data:/loki
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dwengo_postgres_data:
|
|
||||||
dwengo_loki_data:
|
dwengo_loki_data:
|
||||||
dwengo_grafana_data:
|
dwengo_postgres_data:
|
|
@ -620,7 +620,15 @@
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-jwt",
|
"clientAuthenticatorType": "client-jwt",
|
||||||
"redirectUris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost:5173/*", "http://localhost:5173"],
|
"redirectUris": [
|
||||||
|
"urn:ietf:wg:oauth:2.0:oob",
|
||||||
|
"http://localhost:5173/*",
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://localhost/*",
|
||||||
|
"http://localhost",
|
||||||
|
"https://sel2-1.ugent.be/*",
|
||||||
|
"https://sel2-1.ugent.be"
|
||||||
|
],
|
||||||
"webOrigins": ["+"],
|
"webOrigins": ["+"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
|
@ -620,7 +620,15 @@
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"redirectUris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost:5173/*", "http://localhost:5173"],
|
"redirectUris": [
|
||||||
|
"urn:ietf:wg:oauth:2.0:oob",
|
||||||
|
"http://localhost:5173/*",
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://localhost/*",
|
||||||
|
"http://localhost",
|
||||||
|
"https://sel2-1.ugent.be/*",
|
||||||
|
"https://sel2-1.ugent.be"
|
||||||
|
],
|
||||||
"webOrigins": ["+"],
|
"webOrigins": ["+"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
32
config/nginx/nginx.conf
Normal file
32
config/nginx/nginx.conf
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
types {
|
||||||
|
application/javascript mjs;
|
||||||
|
text/css;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
docs/api/generate.ts
Normal file
58
docs/api/generate.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import swaggerAutogen from 'swagger-autogen';
|
||||||
|
|
||||||
|
const doc = {
|
||||||
|
info: {
|
||||||
|
version: '0.1.0',
|
||||||
|
title: 'Dwengo-1 Backend API',
|
||||||
|
description: 'Dwengo-1 Backend API using Express, based on VZW Dwengo',
|
||||||
|
license: {
|
||||||
|
name: 'MIT',
|
||||||
|
url: 'https://github.com/SELab-2/Dwengo-1/blob/336496ab6352ee3f8bf47490c90b5cf81526cef6/LICENSE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: 'http://localhost:3000/',
|
||||||
|
description: 'Development server',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://sel2-1.ugent.be/',
|
||||||
|
description: 'Production server',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
student: {
|
||||||
|
type: 'oauth2',
|
||||||
|
flows: {
|
||||||
|
implicit: {
|
||||||
|
authorizationUrl: 'https://sel2-1.ugent.be/idp/realms/student/protocol/openid-connect/auth',
|
||||||
|
scopes: {
|
||||||
|
openid: 'openid',
|
||||||
|
profile: 'profile',
|
||||||
|
email: 'email',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
teacher: {
|
||||||
|
type: 'oauth2',
|
||||||
|
flows: {
|
||||||
|
implicit: {
|
||||||
|
authorizationUrl: 'https://sel2-1.ugent.be/idp/realms/teacher/protocol/openid-connect/auth',
|
||||||
|
scopes: {
|
||||||
|
openid: 'openid',
|
||||||
|
profile: 'profile',
|
||||||
|
email: 'email',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const outputFile = './swagger.json';
|
||||||
|
const routes = ['../../backend/src/app.ts'];
|
||||||
|
|
||||||
|
swaggerAutogen({ openapi: '3.1.0' })(outputFile, routes, doc);
|
1964
docs/api/swagger.json
Normal file
1964
docs/api/swagger.json
Normal file
File diff suppressed because it is too large
Load diff
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