Merge remote-tracking branch 'origin/dev' into github-actions/deployment
This commit is contained in:
commit
6b23169421
106 changed files with 5455 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
|
|
@ -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_PORT=5431
|
||||
DWENGO_DB_USERNAME=postgres
|
||||
DWENGO_DB_PASSWORD=postgres
|
||||
DWENGO_DB_UPDATE=true
|
||||
|
||||
# Auth
|
||||
|
||||
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
|
||||
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs
|
||||
|
@ -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!
|
||||
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 @@
|
|||
DWENGO_PORT=3000 # The port the backend will listen on
|
||||
#
|
||||
# Basic configuration
|
||||
#
|
||||
|
||||
DWENGO_PORT=3000 # The port the backend will listen on
|
||||
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
|
||||
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_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",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.1",
|
||||
"description": "Backend for Dwengo-1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
@ -34,6 +34,7 @@
|
|||
"loki-logger-ts": "^1.0.2",
|
||||
"marked": "^15.0.7",
|
||||
"response-time": "^2.3.3",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"winston-loki": "^6.1.3"
|
||||
|
@ -45,6 +46,7 @@
|
|||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/response-time": "^2.3.8",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"globals": "^15.15.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"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 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 cors from './middleware/cors.js';
|
||||
import { getLogger, Logger } from './logging/initalize.js';
|
||||
import { responseTimeLogger } from './logging/responseTimeLogger.js';
|
||||
import responseTime from 'response-time';
|
||||
import { EnvVars, getNumericEnvVar } from './util/envvars.js';
|
||||
import apiRouter from './routes/router.js';
|
||||
import swaggerMiddleware from './swagger.js';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
const app: Express = express();
|
||||
const port: string | number = getNumericEnvVar(EnvVars.Port);
|
||||
|
||||
app.use(cors);
|
||||
app.use(express.json());
|
||||
app.use(responseTime(responseTimeLogger));
|
||||
app.use(cors);
|
||||
app.use(authenticateUser);
|
||||
// Add response time logging
|
||||
app.use(responseTime(responseTimeLogger));
|
||||
|
||||
// TODO Replace with Express routes
|
||||
app.get('/', (_, res: Response) => {
|
||||
logger.debug('GET /');
|
||||
res.json({
|
||||
message: 'Hello Dwengo!🚀',
|
||||
});
|
||||
});
|
||||
app.use('/api', apiRouter);
|
||||
|
||||
app.use('/student', studentRouter);
|
||||
app.use('/group', groupRouter);
|
||||
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);
|
||||
// Swagger
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
|
||||
|
||||
async function startServer() {
|
||||
await initORM();
|
||||
|
||||
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 { Language } from './entities/content/language.js';
|
||||
|
||||
// API
|
||||
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
|
||||
|
@ -7,3 +8,5 @@ export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage);
|
|||
// Logging
|
||||
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 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 { themes } from '../data/themes.js';
|
||||
import { loadTranslations } from '../util/translationHelper.js';
|
||||
import { loadTranslations } from '../util/translation-helper.js';
|
||||
|
||||
interface Translations {
|
||||
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 { Group } from '../../entities/assignments/group.entity.js';
|
||||
import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
||||
import { Student } from '../../entities/users/student.entity.js';
|
||||
|
||||
export class GroupRepository extends DwengoEntityRepository<Group> {
|
||||
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
|
||||
return this.findOne({
|
||||
assignment: assignment,
|
||||
groupNumber: groupNumber,
|
||||
});
|
||||
return this.findOne(
|
||||
{
|
||||
assignment: assignment,
|
||||
groupNumber: groupNumber,
|
||||
},
|
||||
{ populate: ['members'] }
|
||||
);
|
||||
}
|
||||
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) {
|
||||
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> {
|
||||
return this.deleteWhere({
|
||||
learningObjectHruid: loId.hruid,
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
import { DwengoEntityRepository } from '../dwengo-entity-repository.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> {
|
||||
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> {
|
||||
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 { LearningObject } from '../../entities/content/learning-object.entity.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> {
|
||||
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 { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||
import { Student } from '../../entities/users/student.entity.js';
|
||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||
|
||||
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
|
||||
|
@ -40,4 +41,17 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
|||
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 */
|
||||
export const getUserRepository = repositoryGetter<User, UserRepository>(User);
|
||||
export const getStudentRepository = repositoryGetter<Student, StudentRepository>(Student);
|
||||
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 { 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> {
|
||||
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 { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||
import { UserRepository } from './user-repository.js';
|
||||
|
||||
export class TeacherRepository extends DwengoEntityRepository<Teacher> {
|
||||
public findByUsername(username: string): Promise<Teacher | null> {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||
import { User } from '../../entities/users/user.entity.js';
|
||||
|
||||
export class UserRepository extends DwengoEntityRepository<User> {
|
||||
public findByUsername(username: string): Promise<User | null> {
|
||||
return this.findOne({ username: username });
|
||||
export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
|
||||
public findByUsername(username: string): Promise<T | null> {
|
||||
return this.findOne({ username } as Partial<T>);
|
||||
}
|
||||
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 { Group } from './group.entity.js';
|
||||
import { Language } from '../content/language.js';
|
||||
import { AssignmentRepository } from '../../data/assignments/assignment-repository.js';
|
||||
|
||||
@Entity({ repository: () => AssignmentRepository })
|
||||
@Entity({
|
||||
repository: () => AssignmentRepository,
|
||||
})
|
||||
export class Assignment {
|
||||
@ManyToOne({ entity: () => Class, primary: true })
|
||||
@ManyToOne({
|
||||
entity: () => Class,
|
||||
primary: true,
|
||||
})
|
||||
within!: Class;
|
||||
|
||||
@PrimaryKey({ type: 'number' })
|
||||
id!: number;
|
||||
@PrimaryKey({ type: 'number', autoincrement: true })
|
||||
id?: number;
|
||||
|
||||
@Property({ type: 'string' })
|
||||
title!: string;
|
||||
|
@ -21,9 +26,14 @@ export class Assignment {
|
|||
@Property({ type: 'string' })
|
||||
learningPathHruid!: string;
|
||||
|
||||
@Enum({ items: () => Language })
|
||||
@Enum({
|
||||
items: () => Language,
|
||||
})
|
||||
learningPathLanguage!: Language;
|
||||
|
||||
@OneToMany({ entity: () => Group, mappedBy: 'assignment' })
|
||||
@OneToMany({
|
||||
entity: () => Group,
|
||||
mappedBy: 'assignment',
|
||||
})
|
||||
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 { Student } from '../users/student.entity.js';
|
||||
import { GroupRepository } from '../../data/assignments/group-repository.js';
|
||||
|
||||
@Entity({ repository: () => GroupRepository })
|
||||
@Entity({
|
||||
repository: () => GroupRepository,
|
||||
})
|
||||
export class Group {
|
||||
@ManyToOne({
|
||||
entity: () => Assignment,
|
||||
|
@ -11,8 +13,8 @@ export class Group {
|
|||
})
|
||||
assignment!: Assignment;
|
||||
|
||||
@PrimaryKey({ type: 'integer' })
|
||||
groupNumber!: number;
|
||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||
groupNumber?: number;
|
||||
|
||||
@ManyToMany({
|
||||
entity: () => Student,
|
||||
|
|
|
@ -18,7 +18,7 @@ export class Submission {
|
|||
@PrimaryKey({ type: 'numeric' })
|
||||
learningObjectVersion: number = 1;
|
||||
|
||||
@PrimaryKey({ type: 'integer' })
|
||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||
submissionNumber!: number;
|
||||
|
||||
@ManyToOne({
|
||||
|
|
|
@ -3,7 +3,9 @@ import { Student } from '../users/student.entity.js';
|
|||
import { Class } from './class.entity.js';
|
||||
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
|
||||
|
||||
@Entity({ repository: () => ClassJoinRequestRepository })
|
||||
@Entity({
|
||||
repository: () => ClassJoinRequestRepository,
|
||||
})
|
||||
export class ClassJoinRequest {
|
||||
@ManyToOne({
|
||||
entity: () => Student,
|
||||
|
|
|
@ -4,10 +4,12 @@ import { Teacher } from '../users/teacher.entity.js';
|
|||
import { Student } from '../users/student.entity.js';
|
||||
import { ClassRepository } from '../../data/classes/class-repository.js';
|
||||
|
||||
@Entity({ repository: () => ClassRepository })
|
||||
@Entity({
|
||||
repository: () => ClassRepository,
|
||||
})
|
||||
export class Class {
|
||||
@PrimaryKey()
|
||||
classId = v4();
|
||||
classId? = v4();
|
||||
|
||||
@Property({ type: '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).
|
||||
*/
|
||||
@Entity({ repository: () => TeacherInvitationRepository })
|
||||
@Entity({
|
||||
repository: () => TeacherInvitationRepository,
|
||||
})
|
||||
export class TeacherInvitation {
|
||||
@ManyToOne({
|
||||
entity: () => Teacher,
|
||||
|
|
|
@ -2,7 +2,9 @@ import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
|||
import { LearningObject } from './learning-object.entity.js';
|
||||
import { AttachmentRepository } from '../../data/content/attachment-repository.js';
|
||||
|
||||
@Entity({ repository: () => AttachmentRepository })
|
||||
@Entity({
|
||||
repository: () => AttachmentRepository,
|
||||
})
|
||||
export class Attachment {
|
||||
@ManyToOne({
|
||||
entity: () => LearningObject,
|
||||
|
|
|
@ -184,3 +184,10 @@ export enum Language {
|
|||
Zhuang = 'za',
|
||||
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) => {
|
||||
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
|
||||
res.json({ message: 'If you see this, you should be authenticated!' });
|
||||
});
|
||||
|
||||
router.get('/testStudentsOnly', studentsOnly, (req, res) => {
|
||||
/* #swagger.security = [{ "student": [ ] }] */
|
||||
res.json({ message: 'If you see this, you should be a student!' });
|
||||
});
|
||||
|
||||
router.get('/testTeachersOnly', teachersOnly, (req, res) => {
|
||||
/* #swagger.security = [{ "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 { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js';
|
||||
|
||||
import submissionRoutes from './submissions.js';
|
||||
import questionRoutes from './questions.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// DWENGO learning objects
|
||||
|
@ -21,6 +24,10 @@ router.get('/', getAllLearningObjects);
|
|||
// Example: http://localhost:3000/learningObject/un_ai7
|
||||
router.get('/:hruid', getLearningObject);
|
||||
|
||||
router.use('/:hruid/submissions', submissionRoutes);
|
||||
|
||||
router.use('/:hruid/:version/questions', questionRoutes);
|
||||
|
||||
// Parameter: hruid of learning object
|
||||
// Query: language, version (optional)
|
||||
// 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 { fetchWithLogging } from '../../util/apiHelper.js';
|
||||
import { fetchWithLogging } from '../../util/api-helper.js';
|
||||
import {
|
||||
FilteredLearningObject,
|
||||
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.');
|
||||
}
|
||||
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> {
|
||||
const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes);
|
||||
|
||||
const targetAges = nodesToLearningObjects
|
||||
.values()
|
||||
.flatMap((it) => it.targetAges || [])
|
||||
.toArray();
|
||||
const targetAges = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.targetAges || []);
|
||||
|
||||
const keywords = nodesToLearningObjects
|
||||
.values()
|
||||
.flatMap((it) => it.keywords || [])
|
||||
.toArray();
|
||||
const keywords = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.keywords || []);
|
||||
|
||||
const image = learningPath.image ? learningPath.image.toString('base64') : undefined;
|
||||
|
||||
|
@ -83,27 +77,24 @@ async function convertNodes(
|
|||
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
|
||||
personalizedFor?: PersonalizationTarget
|
||||
): Promise<LearningObjectNode[]> {
|
||||
const nodesPromise = nodesToLearningObjects
|
||||
.entries()
|
||||
.map(async (entry) => {
|
||||
const [node, learningObject] = entry;
|
||||
const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null;
|
||||
return {
|
||||
_id: learningObject.uuid,
|
||||
language: learningObject.language,
|
||||
start_node: node.startNode,
|
||||
created_at: node.createdAt.toISOString(),
|
||||
updatedAt: node.updatedAt.toISOString(),
|
||||
learningobject_hruid: node.learningObjectHruid,
|
||||
version: learningObject.version,
|
||||
transitions: node.transitions
|
||||
.filter(
|
||||
(trans) => !personalizedFor || isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // If we want a personalized learning path, remove all transitions that aren't possible.
|
||||
)
|
||||
.map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition
|
||||
};
|
||||
})
|
||||
.toArray();
|
||||
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => {
|
||||
const [node, learningObject] = entry;
|
||||
const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null;
|
||||
return {
|
||||
_id: learningObject.uuid,
|
||||
language: learningObject.language,
|
||||
start_node: node.startNode,
|
||||
created_at: node.createdAt.toISOString(),
|
||||
updatedAt: node.updatedAt.toISOString(),
|
||||
learningobject_hruid: node.learningObjectHruid,
|
||||
version: learningObject.version,
|
||||
transitions: node.transitions
|
||||
.filter(
|
||||
(trans) => !personalizedFor || isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // If we want a personalized learning path, remove all transitions that aren't possible.
|
||||
)
|
||||
.map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition
|
||||
};
|
||||
});
|
||||
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 { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.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 () => {
|
||||
const id = new LearningObjectIdentifier('id03', Language.English, '1');
|
||||
const id = new LearningObjectIdentifier('id03', Language.English, 1);
|
||||
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
||||
|
||||
expect(submission).toBeTruthy();
|
||||
|
@ -40,7 +40,7 @@ describe('SubmissionRepository', () => {
|
|||
});
|
||||
|
||||
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 submission = await submissionRepository.findMostRecentSubmissionForStudent(id, student!);
|
||||
|
||||
|
@ -49,7 +49,7 @@ describe('SubmissionRepository', () => {
|
|||
});
|
||||
|
||||
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 assignment = await assignmentRepository.findByClassAndId(class_!, 1);
|
||||
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1);
|
||||
|
@ -60,7 +60,7 @@ describe('SubmissionRepository', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
|
||||
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
||||
|
|
|
@ -17,11 +17,11 @@ describe('AttachmentRepository', () => {
|
|||
});
|
||||
|
||||
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 attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName(
|
||||
learningObject!,
|
||||
learningObject!.hruid,
|
||||
Language.English,
|
||||
'attachment01'
|
||||
);
|
||||
|
|
|
@ -13,8 +13,8 @@ describe('LearningObjectRepository', () => {
|
|||
learningObjectRepository = getLearningObjectRepository();
|
||||
});
|
||||
|
||||
const id01 = new LearningObjectIdentifier('id01', Language.English, '1');
|
||||
const id02 = new LearningObjectIdentifier('test_id', Language.English, '1');
|
||||
const id01 = new LearningObjectIdentifier('id01', Language.English, 1);
|
||||
const id02 = new LearningObjectIdentifier('test_id', Language.English, 1);
|
||||
|
||||
it('should return the learning object that matches identifier 1', async () => {
|
||||
const learningObject = await learningObjectRepository.findByIdentifier(id01);
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('AnswerRepository', () => {
|
|||
});
|
||||
|
||||
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 question = questions.filter((it) => it.sequenceNumber == 2)[0];
|
||||
|
@ -35,7 +35,7 @@ describe('AnswerRepository', () => {
|
|||
|
||||
it('should create an answer to a question', async () => {
|
||||
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 question = questions[0];
|
||||
|
@ -54,7 +54,7 @@ describe('AnswerRepository', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
|
||||
await answerRepository.removeAnswerByQuestionAndSequenceNumber(questions[0], 1);
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('QuestionRepository', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(questions).toBeTruthy();
|
||||
|
@ -28,7 +28,7 @@ describe('QuestionRepository', () => {
|
|||
});
|
||||
|
||||
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');
|
||||
await questionRepository.createQuestion({
|
||||
loId: id,
|
||||
|
@ -42,7 +42,7 @@ describe('QuestionRepository', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
|
||||
const question = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
||||
|
|
|
@ -12,7 +12,7 @@ export function makeTestSubmissions(
|
|||
const submission01 = em.create(Submission, {
|
||||
learningObjectHruid: 'id03',
|
||||
learningObjectLanguage: Language.English,
|
||||
learningObjectVersion: '1',
|
||||
learningObjectVersion: 1,
|
||||
submissionNumber: 1,
|
||||
submitter: students[0],
|
||||
submissionTime: new Date(2025, 2, 20),
|
||||
|
@ -23,7 +23,7 @@ export function makeTestSubmissions(
|
|||
const submission02 = em.create(Submission, {
|
||||
learningObjectHruid: 'id03',
|
||||
learningObjectLanguage: Language.English,
|
||||
learningObjectVersion: '1',
|
||||
learningObjectVersion: 1,
|
||||
submissionNumber: 2,
|
||||
submitter: students[0],
|
||||
submissionTime: new Date(2025, 2, 25),
|
||||
|
@ -34,7 +34,7 @@ export function makeTestSubmissions(
|
|||
const submission03 = em.create(Submission, {
|
||||
learningObjectHruid: 'id02',
|
||||
learningObjectLanguage: Language.English,
|
||||
learningObjectVersion: '1',
|
||||
learningObjectVersion: 1,
|
||||
submissionNumber: 1,
|
||||
submitter: students[0],
|
||||
submissionTime: new Date(2025, 2, 20),
|
||||
|
@ -44,7 +44,7 @@ export function makeTestSubmissions(
|
|||
const submission04 = em.create(Submission, {
|
||||
learningObjectHruid: 'id02',
|
||||
learningObjectLanguage: Language.English,
|
||||
learningObjectVersion: '1',
|
||||
learningObjectVersion: 1,
|
||||
submissionNumber: 2,
|
||||
submitter: students[0],
|
||||
submissionTime: new Date(2025, 2, 25),
|
||||
|
@ -54,7 +54,7 @@ export function makeTestSubmissions(
|
|||
const submission05 = em.create(Submission, {
|
||||
learningObjectHruid: 'id01',
|
||||
learningObjectLanguage: Language.English,
|
||||
learningObjectVersion: '1',
|
||||
learningObjectVersion: 1,
|
||||
submissionNumber: 1,
|
||||
submitter: students[1],
|
||||
submissionTime: new Date(2025, 2, 20),
|
||||
|
|
|
@ -77,7 +77,7 @@ export function makeTestLearningPaths(em: EntityManager<IDatabaseDriver<Connecti
|
|||
admins: [],
|
||||
title: 'repertoire Tool',
|
||||
description: 'all about Tool',
|
||||
image: '',
|
||||
image: null,
|
||||
nodes: nodes01,
|
||||
});
|
||||
|
||||
|
@ -92,7 +92,7 @@ export function makeTestLearningPaths(em: EntityManager<IDatabaseDriver<Connecti
|
|||
admins: [],
|
||||
title: 'repertoire Dire Straits',
|
||||
description: 'all about Dire Straits',
|
||||
image: '',
|
||||
image: null,
|
||||
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> {
|
||||
const question01 = em.create(Question, {
|
||||
learningObjectLanguage: Language.English,
|
||||
learningObjectVersion: '1',
|
||||
learningObjectVersion: 1,
|
||||
learningObjectHruid: 'id05',
|
||||
sequenceNumber: 1,
|
||||
author: students[0],
|
||||
|
@ -16,7 +16,7 @@ export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>
|
|||
|
||||
const question02 = em.create(Question, {
|
||||
learningObjectLanguage: Language.English,
|
||||
learningObjectVersion: '1',
|
||||
learningObjectVersion: 1,
|
||||
learningObjectHruid: 'id05',
|
||||
sequenceNumber: 2,
|
||||
author: students[2],
|
||||
|
@ -26,7 +26,7 @@ export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>
|
|||
|
||||
const question03 = em.create(Question, {
|
||||
learningObjectLanguage: Language.English,
|
||||
learningObjectVersion: '1',
|
||||
learningObjectVersion: 1,
|
||||
learningObjectHruid: 'id04',
|
||||
sequenceNumber: 1,
|
||||
author: students[0],
|
||||
|
@ -36,7 +36,7 @@ export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>
|
|||
|
||||
const question04 = em.create(Question, {
|
||||
learningObjectLanguage: Language.English,
|
||||
learningObjectVersion: '1',
|
||||
learningObjectVersion: 1,
|
||||
learningObjectHruid: 'id01',
|
||||
sequenceNumber: 1,
|
||||
author: students[1],
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"include": ["src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"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:
|
||||
db:
|
||||
image: postgres:latest
|
||||
ports:
|
||||
- '5431:5432'
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- dwengo_postgres_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: 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
|
||||
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:
|
||||
- ./idp:/opt/keycloak/data/import
|
||||
- ./config/idp:/opt/keycloak/data/import
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
KC_HOSTNAME: localhost
|
||||
KC_HOSTNAME_PORT: 7080
|
||||
|
@ -41,19 +35,18 @@ services:
|
|||
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||
KC_HEALTH_ENABLED: 'true'
|
||||
KC_LOG_LEVEL: info
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:7080/health/ready']
|
||||
interval: 15s
|
||||
timeout: 2s
|
||||
retries: 15
|
||||
command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm']
|
||||
|
||||
logging:
|
||||
image: grafana/loki:latest
|
||||
ports:
|
||||
- '7080:7080'
|
||||
- '7443:7443'
|
||||
depends_on:
|
||||
- db
|
||||
- '9001:3102'
|
||||
- '9095:9095'
|
||||
command: -config.file=/etc/loki/config.yaml
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config/loki/config.yml:/etc/loki/config.yaml
|
||||
- dwengo_loki_data:/loki
|
||||
|
||||
volumes:
|
||||
dwengo_postgres_data:
|
||||
dwengo_loki_data:
|
||||
dwengo_grafana_data:
|
||||
dwengo_postgres_data:
|
|
@ -620,7 +620,15 @@
|
|||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"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": ["+"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
|
@ -620,7 +620,15 @@
|
|||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"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": ["+"],
|
||||
"notBefore": 0,
|
||||
"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
14
docs/package.json
Normal file
14
docs/package.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "dwengo-1-docs",
|
||||
"version": "0.0.1",
|
||||
"description": "Documentation for Dwengo-1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npm run architecture && npm run swagger",
|
||||
"architecture": "python3 -m venv .venv && source .venv/bin/activate && pip install -r docs/requirements.txt && python docs/architecture/schema.py",
|
||||
"swagger": "tsx api/generate.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"swagger-autogen": "^2.23.7"
|
||||
}
|
||||
}
|
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