Merge branch 'dev' into feat/pagina-overzicht-klassen-voor-student
This commit is contained in:
commit
aa43a8297d
175 changed files with 4035 additions and 2774 deletions
45
.github/workflows/backend-testing.yml
vendored
Normal file
45
.github/workflows/backend-testing.yml
vendored
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# This workflow will do a clean installation of node dependencies, cache/restore them, run backend tests across different versions of node (here 22.x)
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||||
|
|
||||||
|
name: Backend Testing
|
||||||
|
|
||||||
|
# Workflow runs when:
|
||||||
|
# - a backend js/ts file on "dev" changes
|
||||||
|
# - a non-draft PR to "dev" with backend js/ts files is opened, is reopened, or changes
|
||||||
|
# - a draft PR to "dev" with backend js/ts files is marked as ready for review
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "dev" ]
|
||||||
|
paths:
|
||||||
|
- 'backend/src/**.[jt]s'
|
||||||
|
- 'backend/tests/**.[jt]s'
|
||||||
|
- 'backend/vitest.config.ts'
|
||||||
|
pull_request:
|
||||||
|
branches: [ "dev" ]
|
||||||
|
types: ["synchronize", "ready_for_review", "opened", "reopened"]
|
||||||
|
paths:
|
||||||
|
- 'backend/src/**.[jt]s'
|
||||||
|
- 'backend/tests/**.[jt]s'
|
||||||
|
- 'backend/vitest.config.ts'
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Run backend unit tests
|
||||||
|
if: '! github.event.pull_request.draft'
|
||||||
|
runs-on: [self-hosted, Linux, X64]
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [22.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run test:unit -w backend
|
54
.github/workflows/frontend-testing.yml
vendored
Normal file
54
.github/workflows/frontend-testing.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# This workflow will do a clean installation of node dependencies, cache/restore them, run frontend tests across different versions of node (here 22.x)
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||||
|
|
||||||
|
name: Frontend Testing
|
||||||
|
|
||||||
|
# Workflow runs when:
|
||||||
|
# - a frontend js/ts/vue/css file on "dev" changes
|
||||||
|
# - a non-draft PR to "dev" with frontend js/ts/vue/css files is opened, is reopened, or changes
|
||||||
|
# - a draft PR to "dev" with frontend js/ts/vue/css files is marked as ready for review
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "dev" ]
|
||||||
|
paths:
|
||||||
|
- 'frontend/src/**.[jt]s'
|
||||||
|
- 'frontend/src/**.vue'
|
||||||
|
- 'frontend/src/**.css'
|
||||||
|
- 'frontend/tests/**.[jt]s'
|
||||||
|
- 'frontend/tests/**.vue'
|
||||||
|
- 'frontend/tests/**.css'
|
||||||
|
- 'frontend/vitest.config.ts'
|
||||||
|
- 'frontend/playwright.config.ts'
|
||||||
|
pull_request:
|
||||||
|
branches: [ "dev" ]
|
||||||
|
types: ["synchronize", "ready_for_review", "opened", "reopened"]
|
||||||
|
paths:
|
||||||
|
- 'frontend/src/**.[jt]s'
|
||||||
|
- 'frontend/src/**.vue'
|
||||||
|
- 'frontend/src/**.css'
|
||||||
|
- 'frontend/tests/**.[jt]s'
|
||||||
|
- 'frontend/tests/**.vue'
|
||||||
|
- 'frontend/tests/**.css'
|
||||||
|
- 'frontend/vitest.config.ts'
|
||||||
|
- 'frontend/playwright.config.ts'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Run frontend unit tests
|
||||||
|
if: '! github.event.pull_request.draft'
|
||||||
|
runs-on: [self-hosted, Linux, X64]
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [22.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run test:unit -w frontend
|
|
@ -8,14 +8,4 @@ export default [
|
||||||
globals: globals.node,
|
globals: globals.node,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
files: ['tests/**/*.ts'],
|
|
||||||
languageOptions: {
|
|
||||||
globals: globals.node,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'no-console': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
"format": "prettier --write src/",
|
"format": "prettier --write src/",
|
||||||
"format-check": "prettier --check src/",
|
"format-check": "prettier --check src/",
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
"test:unit": "vitest"
|
"test:unit": "vitest --run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mikro-orm/core": "6.4.9",
|
"@mikro-orm/core": "6.4.9",
|
||||||
|
|
|
@ -5,15 +5,16 @@ import cors from './middleware/cors.js';
|
||||||
import { getLogger, Logger } from './logging/initalize.js';
|
import { getLogger, Logger } from './logging/initalize.js';
|
||||||
import { responseTimeLogger } from './logging/responseTimeLogger.js';
|
import { responseTimeLogger } from './logging/responseTimeLogger.js';
|
||||||
import responseTime from 'response-time';
|
import responseTime from 'response-time';
|
||||||
import { EnvVars, getNumericEnvVar } from './util/envvars.js';
|
import { envVars, getNumericEnvVar } from './util/envVars.js';
|
||||||
import apiRouter from './routes/router.js';
|
import apiRouter from './routes/router.js';
|
||||||
import swaggerMiddleware from './swagger.js';
|
import swaggerMiddleware from './swagger.js';
|
||||||
import swaggerUi from 'swagger-ui-express';
|
import swaggerUi from 'swagger-ui-express';
|
||||||
|
import { errorHandler } from './middleware/error-handling/error-handler.js';
|
||||||
|
|
||||||
const logger: Logger = getLogger();
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
const app: Express = express();
|
const app: Express = express();
|
||||||
const port: string | number = getNumericEnvVar(EnvVars.Port);
|
const port: string | number = getNumericEnvVar(envVars.Port);
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cors);
|
app.use(cors);
|
||||||
|
@ -26,7 +27,9 @@ app.use('/api', apiRouter);
|
||||||
// Swagger
|
// Swagger
|
||||||
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
|
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
|
||||||
|
|
||||||
async function startServer() {
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
async function startServer(): Promise<void> {
|
||||||
await initORM();
|
await initORM();
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { EnvVars, getEnvVar } from './util/envvars.js';
|
import { envVars, getEnvVar } from './util/envVars.js';
|
||||||
|
|
||||||
// API
|
// API
|
||||||
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
|
export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl);
|
||||||
export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage);
|
export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage);
|
||||||
|
|
||||||
export const FALLBACK_SEQ_NUM = 1;
|
export const FALLBACK_SEQ_NUM = 1;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Request, Response } from 'express';
|
||||||
import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js';
|
import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js';
|
||||||
import { AssignmentDTO } from '../interfaces/assignment.js';
|
import { AssignmentDTO } from '../interfaces/assignment.js';
|
||||||
|
|
||||||
// Typescript is annoy with with parameter forwarding from class.ts
|
// Typescript is annoying with parameter forwarding from class.ts
|
||||||
interface AssignmentParams {
|
interface AssignmentParams {
|
||||||
classid: string;
|
classid: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -37,11 +37,11 @@ export async function createAssignmentHandler(req: Request<AssignmentParams>, re
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ assignment: assignment });
|
res.status(201).json(assignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
||||||
const id = +req.params.id;
|
const id = Number(req.params.id);
|
||||||
const classid = req.params.classid;
|
const classid = req.params.classid;
|
||||||
|
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
|
@ -61,14 +61,15 @@ export async function getAssignmentHandler(req: Request<AssignmentParams>, res:
|
||||||
|
|
||||||
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
||||||
const classid = req.params.classid;
|
const classid = req.params.classid;
|
||||||
const assignmentNumber = +req.params.id;
|
const assignmentNumber = Number(req.params.id);
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
if (isNaN(assignmentNumber)) {
|
if (isNaN(assignmentNumber)) {
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber);
|
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
submissions: submissions,
|
submissions: submissions,
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||||
|
|
||||||
type FrontendIdpConfig = {
|
interface FrontendIdpConfig {
|
||||||
authority: string;
|
authority: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
responseType: string;
|
responseType: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
type FrontendAuthConfig = {
|
interface FrontendAuthConfig {
|
||||||
student: FrontendIdpConfig;
|
student: FrontendIdpConfig;
|
||||||
teacher: FrontendIdpConfig;
|
teacher: FrontendIdpConfig;
|
||||||
};
|
}
|
||||||
|
|
||||||
const SCOPE = 'openid profile email';
|
const SCOPE = 'openid profile email';
|
||||||
const RESPONSE_TYPE = 'code';
|
const RESPONSE_TYPE = 'code';
|
||||||
|
@ -18,14 +18,14 @@ const RESPONSE_TYPE = 'code';
|
||||||
export function getFrontendAuthConfig(): FrontendAuthConfig {
|
export function getFrontendAuthConfig(): FrontendAuthConfig {
|
||||||
return {
|
return {
|
||||||
student: {
|
student: {
|
||||||
authority: getEnvVar(EnvVars.IdpStudentUrl),
|
authority: getEnvVar(envVars.IdpStudentUrl),
|
||||||
clientId: getEnvVar(EnvVars.IdpStudentClientId),
|
clientId: getEnvVar(envVars.IdpStudentClientId),
|
||||||
scope: SCOPE,
|
scope: SCOPE,
|
||||||
responseType: RESPONSE_TYPE,
|
responseType: RESPONSE_TYPE,
|
||||||
},
|
},
|
||||||
teacher: {
|
teacher: {
|
||||||
authority: getEnvVar(EnvVars.IdpTeacherUrl),
|
authority: getEnvVar(envVars.IdpTeacherUrl),
|
||||||
clientId: getEnvVar(EnvVars.IdpTeacherClientId),
|
clientId: getEnvVar(envVars.IdpTeacherClientId),
|
||||||
scope: SCOPE,
|
scope: SCOPE,
|
||||||
responseType: RESPONSE_TYPE,
|
responseType: RESPONSE_TYPE,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/class.js';
|
import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/classes.js';
|
||||||
import { ClassDTO } from '../interfaces/class.js';
|
import { ClassDTO } from '../interfaces/class.js';
|
||||||
|
|
||||||
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
|
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -28,30 +28,19 @@ export async function createClassHandler(req: Request, res: Response): Promise<v
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ class: cls });
|
res.status(201).json(cls);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getClassHandler(req: Request, res: Response): Promise<void> {
|
export async function getClassHandler(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
const classId = req.params.id;
|
||||||
const classId = req.params.id;
|
const cls = await getClass(classId);
|
||||||
const cls = await getClass(classId);
|
|
||||||
|
|
||||||
if (!cls) {
|
if (!cls) {
|
||||||
res.status(404).json({ error: 'Class not found' });
|
res.status(404).json({ error: 'Class not found' });
|
||||||
return;
|
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' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.json(cls);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
|
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -67,7 +56,7 @@ export async function getClassStudentsHandler(req: Request, res: Response): Prom
|
||||||
|
|
||||||
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const classId = req.params.id;
|
const classId = req.params.id;
|
||||||
const full = req.query.full === 'true'; // TODO: not implemented yet
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const invitations = await getClassTeacherInvitations(classId, full);
|
const invitations = await getClassTeacherInvitations(classId, full);
|
||||||
|
|
||||||
|
|
|
@ -12,14 +12,14 @@ interface GroupParams {
|
||||||
export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> {
|
export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> {
|
||||||
const classId = req.params.classid;
|
const classId = req.params.classid;
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
const assignmentId = +req.params.assignmentid;
|
const assignmentId = Number(req.params.assignmentid);
|
||||||
|
|
||||||
if (isNaN(assignmentId)) {
|
if (isNaN(assignmentId)) {
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupId = +req.params.groupid!; // Can't be undefined
|
const groupId = Number(req.params.groupid!); // Can't be undefined
|
||||||
|
|
||||||
if (isNaN(groupId)) {
|
if (isNaN(groupId)) {
|
||||||
res.status(400).json({ error: 'Group id must be a number' });
|
res.status(400).json({ error: 'Group id must be a number' });
|
||||||
|
@ -28,6 +28,11 @@ export async function getGroupHandler(req: Request<GroupParams>, res: Response):
|
||||||
|
|
||||||
const group = await getGroup(classId, assignmentId, groupId, full);
|
const group = await getGroup(classId, assignmentId, groupId, full);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
res.status(404).json({ error: 'Group not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
res.json(group);
|
res.json(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +40,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise<
|
||||||
const classId = req.params.classid;
|
const classId = req.params.classid;
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const assignmentId = +req.params.assignmentid;
|
const assignmentId = Number(req.params.assignmentid);
|
||||||
|
|
||||||
if (isNaN(assignmentId)) {
|
if (isNaN(assignmentId)) {
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||||
|
@ -51,7 +56,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise<
|
||||||
|
|
||||||
export async function createGroupHandler(req: Request, res: Response): Promise<void> {
|
export async function createGroupHandler(req: Request, res: Response): Promise<void> {
|
||||||
const classid = req.params.classid;
|
const classid = req.params.classid;
|
||||||
const assignmentId = +req.params.assignmentid;
|
const assignmentId = Number(req.params.assignmentid);
|
||||||
|
|
||||||
if (isNaN(assignmentId)) {
|
if (isNaN(assignmentId)) {
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||||
|
@ -66,28 +71,28 @@ export async function createGroupHandler(req: Request, res: Response): Promise<v
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ group: group });
|
res.status(201).json(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const classId = req.params.classid;
|
const classId = req.params.classid;
|
||||||
// Const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const assignmentId = +req.params.assignmentid;
|
const assignmentId = Number(req.params.assignmentid);
|
||||||
|
|
||||||
if (isNaN(assignmentId)) {
|
if (isNaN(assignmentId)) {
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupId = +req.params.groupid!; // Can't be undefined
|
const groupId = Number(req.params.groupid); // Can't be undefined
|
||||||
|
|
||||||
if (isNaN(groupId)) {
|
if (isNaN(groupId)) {
|
||||||
res.status(400).json({ error: 'Group id must be a number' });
|
res.status(400).json({ error: 'Group id must be a number' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissions = await getGroupSubmissions(classId, assignmentId, groupId);
|
const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
submissions: submissions,
|
submissions: submissions,
|
||||||
|
|
|
@ -2,19 +2,19 @@ import { Request, Response } from 'express';
|
||||||
import { FALLBACK_LANG } from '../config.js';
|
import { FALLBACK_LANG } from '../config.js';
|
||||||
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js';
|
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js';
|
||||||
import learningObjectService from '../services/learning-objects/learning-object-service.js';
|
import learningObjectService from '../services/learning-objects/learning-object-service.js';
|
||||||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||||
import { Language } from '../entities/content/language.js';
|
import { Language } from '../entities/content/language.js';
|
||||||
import { BadRequestException } from '../exceptions.js';
|
|
||||||
import attachmentService from '../services/learning-objects/attachment-service.js';
|
import attachmentService from '../services/learning-objects/attachment-service.js';
|
||||||
import { NotFoundError } from '@mikro-orm/core';
|
import { NotFoundError } from '@mikro-orm/core';
|
||||||
|
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||||
|
|
||||||
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier {
|
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier {
|
||||||
if (!req.params.hruid) {
|
if (!req.params.hruid) {
|
||||||
throw new BadRequestException('HRUID is required.');
|
throw new BadRequestException('HRUID is required.');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
hruid: req.params.hruid as string,
|
hruid: req.params.hruid,
|
||||||
language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language,
|
language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language,
|
||||||
version: parseInt(req.query.version as string),
|
version: parseInt(req.query.version as string),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif
|
||||||
throw new BadRequestException('HRUID is required.');
|
throw new BadRequestException('HRUID is required.');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
hruid: req.params.hruid as string,
|
hruid: req.params.hruid,
|
||||||
language: (req.query.language as Language) || FALLBACK_LANG,
|
language: (req.query.language as Language) || FALLBACK_LANG,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ export async function getAllLearningObjects(req: Request, res: Response): Promis
|
||||||
learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId);
|
learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(learningObjects);
|
res.json({ learningObjects: learningObjects });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLearningObject(req: Request, res: Response): Promise<void> {
|
export async function getLearningObject(req: Request, res: Response): Promise<void> {
|
||||||
|
|
|
@ -2,13 +2,14 @@ import { Request, Response } from 'express';
|
||||||
import { themes } from '../data/themes.js';
|
import { themes } from '../data/themes.js';
|
||||||
import { FALLBACK_LANG } from '../config.js';
|
import { FALLBACK_LANG } from '../config.js';
|
||||||
import learningPathService from '../services/learning-paths/learning-path-service.js';
|
import learningPathService from '../services/learning-paths/learning-path-service.js';
|
||||||
import { BadRequestException, NotFoundException } from '../exceptions.js';
|
|
||||||
import { Language } from '../entities/content/language.js';
|
import { Language } from '../entities/content/language.js';
|
||||||
import {
|
import {
|
||||||
PersonalizationTarget,
|
PersonalizationTarget,
|
||||||
personalizedForGroup,
|
personalizedForGroup,
|
||||||
personalizedForStudent,
|
personalizedForStudent,
|
||||||
} from '../services/learning-paths/learning-path-personalization-util.js';
|
} from '../services/learning-paths/learning-path-personalization-util.js';
|
||||||
|
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||||
|
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch learning paths based on query parameters.
|
* Fetch learning paths based on query parameters.
|
||||||
|
|
|
@ -17,7 +17,7 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu
|
||||||
return {
|
return {
|
||||||
hruid,
|
hruid,
|
||||||
language: (lang as Language) || FALLBACK_LANG,
|
language: (lang as Language) || FALLBACK_LANG,
|
||||||
version: +version,
|
version: Number(version),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi
|
||||||
if (!questions) {
|
if (!questions) {
|
||||||
res.status(404).json({ error: `Questions not found.` });
|
res.status(404).json({ error: `Questions not found.` });
|
||||||
} else {
|
} else {
|
||||||
res.json(questions);
|
res.json({ questions: questions });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,12 +76,12 @@ export async function getQuestionAnswersHandler(req: Request, res: Response): Pr
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const answers = getAnswersByQuestion(questionId, full);
|
const answers = await getAnswersByQuestion(questionId, full);
|
||||||
|
|
||||||
if (!answers) {
|
if (!answers) {
|
||||||
res.status(404).json({ error: `Questions not found.` });
|
res.status(404).json({ error: `Questions not found` });
|
||||||
} else {
|
} else {
|
||||||
res.json(answers);
|
res.json({ answers: answers });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ export async function createQuestionHandler(req: Request, res: Response): Promis
|
||||||
const question = await createQuestion(questionDTO);
|
const question = await createQuestion(questionDTO);
|
||||||
|
|
||||||
if (!question) {
|
if (!question) {
|
||||||
res.status(400).json({ error: 'Could not add question' });
|
res.status(400).json({ error: 'Could not create question' });
|
||||||
} else {
|
} else {
|
||||||
res.json(question);
|
res.json(question);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,29 +9,21 @@ import {
|
||||||
getStudentGroups,
|
getStudentGroups,
|
||||||
getStudentSubmissions,
|
getStudentSubmissions,
|
||||||
} from '../services/students.js';
|
} 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 { StudentDTO } from '../interfaces/student.js';
|
||||||
import { getStudentRepository } from '../data/repositories.js';
|
|
||||||
import { UserDTO } from '../interfaces/user.js';
|
|
||||||
|
|
||||||
// TODO: accept arguments (full, ...)
|
// TODO: accept arguments (full, ...)
|
||||||
// TODO: endpoints
|
// TODO: endpoints
|
||||||
export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> {
|
export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const studentRepository = getStudentRepository();
|
const students = await getAllStudents(full);
|
||||||
|
|
||||||
const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents();
|
|
||||||
|
|
||||||
if (!students) {
|
if (!students) {
|
||||||
res.status(404).json({ error: `Student not found.` });
|
res.status(404).json({ error: `Student not found.` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json(students);
|
res.json({ students: students });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStudentHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -51,10 +43,10 @@ export async function getStudentHandler(req: Request, res: Response): Promise<vo
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json(user);
|
res.json(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createStudentHandler(req: Request, res: Response) {
|
export async function createStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
const userData = req.body as StudentDTO;
|
const userData = req.body as StudentDTO;
|
||||||
|
|
||||||
if (!userData.username || !userData.firstName || !userData.lastName) {
|
if (!userData.username || !userData.firstName || !userData.lastName) {
|
||||||
|
@ -65,10 +57,18 @@ export async function createStudentHandler(req: Request, res: Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = await createStudent(userData);
|
const newUser = await createStudent(userData);
|
||||||
|
|
||||||
|
if (!newUser) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Something went wrong while creating student',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json(newUser);
|
res.status(201).json(newUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteStudentHandler(req: Request, res: Response) {
|
export async function deleteStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
const username = req.params.username;
|
const username = req.params.username;
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
|
@ -88,25 +88,12 @@ export async function deleteStudentHandler(req: Request, res: Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
const username = req.params.id;
|
||||||
const username = req.params.id;
|
|
||||||
|
|
||||||
const classes = await getStudentClasses(username, full);
|
const classes = await getStudentClasses(username, full);
|
||||||
|
|
||||||
res.json({
|
res.json({ classes: classes });
|
||||||
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
|
// TODO
|
||||||
|
@ -137,8 +124,9 @@ export async function getStudentGroupsHandler(req: Request, res: Response): Prom
|
||||||
|
|
||||||
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const username = req.params.id;
|
const username = req.params.id;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const submissions = await getStudentSubmissions(username);
|
const submissions = await getStudentSubmissions(username, full);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
submissions: submissions,
|
submissions: submissions,
|
||||||
|
|
|
@ -10,7 +10,7 @@ interface SubmissionParams {
|
||||||
|
|
||||||
export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> {
|
export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> {
|
||||||
const lohruid = req.params.hruid;
|
const lohruid = req.params.hruid;
|
||||||
const submissionNumber = +req.params.id;
|
const submissionNumber = Number(req.params.id);
|
||||||
|
|
||||||
if (isNaN(submissionNumber)) {
|
if (isNaN(submissionNumber)) {
|
||||||
res.status(400).json({ error: 'Submission number is not a number' });
|
res.status(400).json({ error: 'Submission number is not a number' });
|
||||||
|
@ -30,21 +30,22 @@ export async function getSubmissionHandler(req: Request<SubmissionParams>, res:
|
||||||
res.json(submission);
|
res.json(submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSubmissionHandler(req: Request, res: Response) {
|
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||||
const submissionDTO = req.body as SubmissionDTO;
|
const submissionDTO = req.body as SubmissionDTO;
|
||||||
|
|
||||||
const submission = await createSubmission(submissionDTO);
|
const submission = await createSubmission(submissionDTO);
|
||||||
|
|
||||||
if (!submission) {
|
if (!submission) {
|
||||||
res.status(404).json({ error: 'Submission not added' });
|
res.status(400).json({ error: 'Failed to create submission' });
|
||||||
} else {
|
return;
|
||||||
res.json(submission);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.json(submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSubmissionHandler(req: Request, res: Response) {
|
export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||||
const hruid = req.params.hruid;
|
const hruid = req.params.hruid;
|
||||||
const submissionNumber = +req.params.id;
|
const submissionNumber = Number(req.params.id);
|
||||||
|
|
||||||
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
||||||
const version = (req.query.version || 1) as number;
|
const version = (req.query.version || 1) as number;
|
||||||
|
@ -53,7 +54,8 @@ export async function deleteSubmissionHandler(req: Request, res: Response) {
|
||||||
|
|
||||||
if (!submission) {
|
if (!submission) {
|
||||||
res.status(404).json({ error: 'Submission not found' });
|
res.status(404).json({ error: 'Submission not found' });
|
||||||
} else {
|
return;
|
||||||
res.json(submission);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.json(submission);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,33 +4,23 @@ import {
|
||||||
deleteTeacher,
|
deleteTeacher,
|
||||||
getAllTeachers,
|
getAllTeachers,
|
||||||
getClassesByTeacher,
|
getClassesByTeacher,
|
||||||
getClassIdsByTeacher,
|
|
||||||
getQuestionIdsByTeacher,
|
|
||||||
getQuestionsByTeacher,
|
getQuestionsByTeacher,
|
||||||
getStudentIdsByTeacher,
|
|
||||||
getStudentsByTeacher,
|
getStudentsByTeacher,
|
||||||
getTeacher,
|
getTeacher,
|
||||||
} from '../services/teachers.js';
|
} 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 { TeacherDTO } from '../interfaces/teacher.js';
|
||||||
import { getTeacherRepository } from '../data/repositories.js';
|
|
||||||
|
|
||||||
export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> {
|
export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> {
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const teacherRepository = getTeacherRepository();
|
const teachers = await getAllTeachers(full);
|
||||||
|
|
||||||
const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers();
|
|
||||||
|
|
||||||
if (!teachers) {
|
if (!teachers) {
|
||||||
res.status(404).json({ error: `Teacher not found.` });
|
res.status(404).json({ error: `Teacher not found.` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json(teachers);
|
res.json({ teachers: teachers });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -45,15 +35,15 @@ export async function getTeacherHandler(req: Request, res: Response): Promise<vo
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
error: `User with username '${username}' not found.`,
|
error: `Teacher '${username}' not found.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json(user);
|
res.json(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTeacherHandler(req: Request, res: Response) {
|
export async function createTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||||
const userData = req.body as TeacherDTO;
|
const userData = req.body as TeacherDTO;
|
||||||
|
|
||||||
if (!userData.username || !userData.firstName || !userData.lastName) {
|
if (!userData.username || !userData.firstName || !userData.lastName) {
|
||||||
|
@ -64,10 +54,16 @@ export async function createTeacherHandler(req: Request, res: Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = await createTeacher(userData);
|
const newUser = await createTeacher(userData);
|
||||||
|
|
||||||
|
if (!newUser) {
|
||||||
|
res.status(400).json({ error: 'Failed to create teacher' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json(newUser);
|
res.status(201).json(newUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTeacherHandler(req: Request, res: Response) {
|
export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||||
const username = req.params.username;
|
const username = req.params.username;
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
|
@ -78,7 +74,7 @@ export async function deleteTeacherHandler(req: Request, res: Response) {
|
||||||
const deletedUser = await deleteTeacher(username);
|
const deletedUser = await deleteTeacher(username);
|
||||||
if (!deletedUser) {
|
if (!deletedUser) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
error: `User with username '${username}' not found.`,
|
error: `User '${username}' not found.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -87,58 +83,58 @@ export async function deleteTeacherHandler(req: Request, res: Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
const username = req.params.username;
|
||||||
const username = req.params.username as string;
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
return;
|
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' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const classes = await getClassesByTeacher(username, full);
|
||||||
|
|
||||||
|
if (!classes) {
|
||||||
|
res.status(404).json({ error: 'Teacher not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ classes: classes });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
const username = req.params.username;
|
||||||
const username = req.params.username as string;
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
return;
|
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' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const students = await getStudentsByTeacher(username, full);
|
||||||
|
|
||||||
|
if (!students) {
|
||||||
|
res.status(404).json({ error: 'Teacher not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ students: students });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
const username = req.params.username;
|
||||||
const username = req.params.username as string;
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
return;
|
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' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const questions = await getQuestionsByTeacher(username, full);
|
||||||
|
|
||||||
|
if (!questions) {
|
||||||
|
res.status(404).json({ error: 'Teacher not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ questions: questions });
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,25 +3,23 @@ import { themes } from '../data/themes.js';
|
||||||
import { loadTranslations } from '../util/translation-helper.js';
|
import { loadTranslations } from '../util/translation-helper.js';
|
||||||
|
|
||||||
interface Translations {
|
interface Translations {
|
||||||
curricula_page: {
|
curricula_page: Record<string, { title: string; description?: string }>;
|
||||||
[key: string]: { title: string; description?: string };
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThemesHandler(req: Request, res: Response) {
|
export function getThemesHandler(req: Request, res: Response): void {
|
||||||
const language = (req.query.language as string)?.toLowerCase() || 'nl';
|
const language = ((req.query.language as string) || 'nl').toLowerCase();
|
||||||
const translations = loadTranslations<Translations>(language);
|
const translations = loadTranslations<Translations>(language);
|
||||||
const themeList = themes.map((theme) => ({
|
const themeList = themes.map((theme) => ({
|
||||||
key: theme.title,
|
key: theme.title,
|
||||||
title: translations.curricula_page[theme.title]?.title || theme.title,
|
title: translations.curricula_page[theme.title].title || theme.title,
|
||||||
description: translations.curricula_page[theme.title]?.description,
|
description: translations.curricula_page[theme.title].description,
|
||||||
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
|
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json(themeList);
|
res.json(themeList);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHruidsByThemeHandler(req: Request, res: Response) {
|
export function getHruidsByThemeHandler(req: Request, res: Response): void {
|
||||||
const themeKey = req.params.theme;
|
const themeKey = req.params.theme;
|
||||||
|
|
||||||
if (!themeKey) {
|
if (!themeKey) {
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
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' });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,13 +3,13 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
||||||
import { Class } from '../../entities/classes/class.entity.js';
|
import { Class } from '../../entities/classes/class.entity.js';
|
||||||
|
|
||||||
export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
|
export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
|
||||||
public findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
|
public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
|
||||||
return this.findOne({ within: within, id: id });
|
return this.findOne({ within: within, id: id });
|
||||||
}
|
}
|
||||||
public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
|
public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
|
||||||
return this.findAll({ where: { within: within } });
|
return this.findAll({ where: { within: within } });
|
||||||
}
|
}
|
||||||
public deleteByClassAndId(within: Class, id: number): Promise<void> {
|
public async deleteByClassAndId(within: Class, id: number): Promise<void> {
|
||||||
return this.deleteWhere({ within: within, id: id });
|
return this.deleteWhere({ within: within, id: id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
|
|
||||||
export class GroupRepository extends DwengoEntityRepository<Group> {
|
export class GroupRepository extends DwengoEntityRepository<Group> {
|
||||||
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
|
public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
assignment: assignment,
|
assignment: assignment,
|
||||||
|
@ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository<Group> {
|
||||||
{ populate: ['members'] }
|
{ populate: ['members'] }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
|
public async findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
|
||||||
return this.findAll({
|
return this.findAll({
|
||||||
where: { assignment: assignment },
|
where: { assignment: assignment },
|
||||||
populate: ['members'],
|
populate: ['members'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public findAllGroupsWithStudent(student: Student): Promise<Group[]> {
|
public async findAllGroupsWithStudent(student: Student): Promise<Group[]> {
|
||||||
return this.find({ members: student }, { populate: ['members'] });
|
return this.find({ members: student }, { populate: ['members'] });
|
||||||
}
|
}
|
||||||
public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) {
|
public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
assignment: assignment,
|
assignment: assignment,
|
||||||
groupNumber: groupNumber,
|
groupNumber: groupNumber,
|
||||||
|
|
|
@ -5,7 +5,10 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
|
|
||||||
export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
||||||
public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission | null> {
|
public async findSubmissionByLearningObjectAndSubmissionNumber(
|
||||||
|
loId: LearningObjectIdentifier,
|
||||||
|
submissionNumber: number
|
||||||
|
): Promise<Submission | null> {
|
||||||
return this.findOne({
|
return this.findOne({
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
learningObjectLanguage: loId.language,
|
learningObjectLanguage: loId.language,
|
||||||
|
@ -14,7 +17,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
|
public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
|
@ -26,7 +29,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> {
|
public async findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
|
@ -38,15 +41,15 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
|
public async findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
|
||||||
return this.find({ onBehalfOf: group });
|
return this.find({ onBehalfOf: group });
|
||||||
}
|
}
|
||||||
|
|
||||||
public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
|
public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
|
||||||
return this.find({ submitter: student });
|
return this.find({ submitter: student });
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
|
public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
learningObjectLanguage: loId.language,
|
learningObjectLanguage: loId.language,
|
||||||
|
|
|
@ -4,13 +4,13 @@ import { ClassJoinRequest } from '../../entities/classes/class-join-request.enti
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
|
|
||||||
export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> {
|
export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> {
|
||||||
public findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
|
public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
|
||||||
return this.findAll({ where: { requester: requester } });
|
return this.findAll({ where: { requester: requester } });
|
||||||
}
|
}
|
||||||
public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
|
public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
|
||||||
return this.findAll({ where: { class: clazz } });
|
return this.findAll({ where: { class: clazz } });
|
||||||
}
|
}
|
||||||
public deleteBy(requester: Student, clazz: Class): Promise<void> {
|
public async deleteBy(requester: Student, clazz: Class): Promise<void> {
|
||||||
return this.deleteWhere({ requester: requester, class: clazz });
|
return this.deleteWhere({ requester: requester, class: clazz });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js';
|
||||||
import { Teacher } from '../../entities/users/teacher.entity';
|
import { Teacher } from '../../entities/users/teacher.entity';
|
||||||
|
|
||||||
export class ClassRepository extends DwengoEntityRepository<Class> {
|
export class ClassRepository extends DwengoEntityRepository<Class> {
|
||||||
public findById(id: string): Promise<Class | null> {
|
public async findById(id: string): Promise<Class | null> {
|
||||||
return this.findOne({ classId: id }, { populate: ['students', 'teachers'] });
|
return this.findOne({ classId: id }, { populate: ['students', 'teachers'] });
|
||||||
}
|
}
|
||||||
public deleteById(id: string): Promise<void> {
|
public async deleteById(id: string): Promise<void> {
|
||||||
return this.deleteWhere({ classId: id });
|
return this.deleteWhere({ classId: id });
|
||||||
}
|
}
|
||||||
public findByStudent(student: Student): Promise<Class[]> {
|
public async findByStudent(student: Student): Promise<Class[]> {
|
||||||
return this.find(
|
return this.find(
|
||||||
{ students: student },
|
{ students: student },
|
||||||
{ populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
|
{ populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findByTeacher(teacher: Teacher): Promise<Class[]> {
|
public async findByTeacher(teacher: Teacher): Promise<Class[]> {
|
||||||
return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] });
|
return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,16 @@ import { TeacherInvitation } from '../../entities/classes/teacher-invitation.ent
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||||
|
|
||||||
export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> {
|
export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> {
|
||||||
public findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> {
|
public async findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> {
|
||||||
return this.findAll({ where: { class: clazz } });
|
return this.findAll({ where: { class: clazz } });
|
||||||
}
|
}
|
||||||
public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
|
public async findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
|
||||||
return this.findAll({ where: { sender: sender } });
|
return this.findAll({ where: { sender: sender } });
|
||||||
}
|
}
|
||||||
public findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> {
|
public async findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> {
|
||||||
return this.findAll({ where: { receiver: receiver } });
|
return this.findAll({ where: { receiver: receiver } });
|
||||||
}
|
}
|
||||||
public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
|
public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
sender: sender,
|
sender: sender,
|
||||||
receiver: receiver,
|
receiver: receiver,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Language } from '../../entities/content/language';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier';
|
||||||
|
|
||||||
export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
|
export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
|
||||||
public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> {
|
public async findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> {
|
||||||
return this.findOne({
|
return this.findOne({
|
||||||
learningObject: {
|
learningObject: {
|
||||||
hruid: learningObjectId.hruid,
|
hruid: learningObjectId.hruid,
|
||||||
|
@ -15,7 +15,11 @@ export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise<Attachment | null> {
|
public async findByMostRecentVersionOfLearningObjectAndName(
|
||||||
|
hruid: string,
|
||||||
|
language: Language,
|
||||||
|
attachmentName: string
|
||||||
|
): Promise<Attachment | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
learningObject: {
|
learningObject: {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Language } from '../../entities/content/language.js';
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||||
|
|
||||||
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
||||||
public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
hruid: identifier.hruid,
|
hruid: identifier.hruid,
|
||||||
|
@ -18,7 +18,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findLatestByHruidAndLanguage(hruid: string, language: Language) {
|
public async findLatestByHruidAndLanguage(hruid: string, language: Language): Promise<LearningObject | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
hruid: hruid,
|
hruid: hruid,
|
||||||
|
@ -33,7 +33,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
||||||
return this.find(
|
return this.find(
|
||||||
{ admins: teacher },
|
{ admins: teacher },
|
||||||
{ populate: ['admins'] } // Make sure to load admin relations
|
{ populate: ['admins'] } // Make sure to load admin relations
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { LearningPath } from '../../entities/content/learning-path.entity.js';
|
||||||
import { Language } from '../../entities/content/language.js';
|
import { Language } from '../../entities/content/language.js';
|
||||||
|
|
||||||
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
|
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
|
||||||
public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
|
public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
|
||||||
return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] });
|
return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
|
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
|
||||||
|
import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js';
|
||||||
|
|
||||||
export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
|
export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
|
||||||
public async save(entity: T) {
|
public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> {
|
||||||
const em = this.getEntityManager();
|
if (options?.preventOverwrite && (await this.findOne(entity))) {
|
||||||
em.persist(entity);
|
throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`);
|
||||||
await em.flush();
|
}
|
||||||
|
await this.getEntityManager().persistAndFlush(entity);
|
||||||
}
|
}
|
||||||
public async deleteWhere(query: FilterQuery<T>) {
|
public async deleteWhere(query: FilterQuery<T>): Promise<void> {
|
||||||
const toDelete = await this.findOne(query);
|
const toDelete = await this.findOne(query);
|
||||||
const em = this.getEntityManager();
|
const em = this.getEntityManager();
|
||||||
if (toDelete) {
|
if (toDelete) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Question } from '../../entities/questions/question.entity.js';
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||||
|
|
||||||
export class AnswerRepository extends DwengoEntityRepository<Answer> {
|
export class AnswerRepository extends DwengoEntityRepository<Answer> {
|
||||||
public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
|
public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
|
||||||
const answerEntity = this.create({
|
const answerEntity = this.create({
|
||||||
toQuestion: answer.toQuestion,
|
toQuestion: answer.toQuestion,
|
||||||
author: answer.author,
|
author: answer.author,
|
||||||
|
@ -13,13 +13,13 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> {
|
||||||
});
|
});
|
||||||
return this.insert(answerEntity);
|
return this.insert(answerEntity);
|
||||||
}
|
}
|
||||||
public findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
|
public async findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
|
||||||
return this.findAll({
|
return this.findAll({
|
||||||
where: { toQuestion: question },
|
where: { toQuestion: question },
|
||||||
orderBy: { sequenceNumber: 'ASC' },
|
orderBy: { sequenceNumber: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
|
public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
toQuestion: question,
|
toQuestion: question,
|
||||||
sequenceNumber: sequenceNumber,
|
sequenceNumber: sequenceNumber,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Student } from '../../entities/users/student.entity.js';
|
||||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||||
|
|
||||||
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||||
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
|
public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
|
||||||
const questionEntity = this.create({
|
const questionEntity = this.create({
|
||||||
learningObjectHruid: question.loId.hruid,
|
learningObjectHruid: question.loId.hruid,
|
||||||
learningObjectLanguage: question.loId.language,
|
learningObjectLanguage: question.loId.language,
|
||||||
|
@ -21,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||||
questionEntity.content = question.content;
|
questionEntity.content = question.content;
|
||||||
return this.insert(questionEntity);
|
return this.insert(questionEntity);
|
||||||
}
|
}
|
||||||
public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
|
public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
|
||||||
return this.findAll({
|
return this.findAll({
|
||||||
where: {
|
where: {
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
|
@ -33,7 +33,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
|
public async removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
learningObjectLanguage: loId.language,
|
learningObjectLanguage: loId.language,
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-o
|
||||||
import { forkEntityManager } from '../orm.js';
|
import { forkEntityManager } from '../orm.js';
|
||||||
import { StudentRepository } from './users/student-repository.js';
|
import { StudentRepository } from './users/student-repository.js';
|
||||||
import { Student } from '../entities/users/student.entity.js';
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
import { User } from '../entities/users/user.entity.js';
|
|
||||||
import { UserRepository } from './users/user-repository.js';
|
|
||||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
import { TeacherRepository } from './users/teacher-repository.js';
|
import { TeacherRepository } from './users/teacher-repository.js';
|
||||||
import { Class } from '../entities/classes/class.entity.js';
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
|
@ -36,8 +34,8 @@ let entityManager: EntityManager | undefined;
|
||||||
/**
|
/**
|
||||||
* Execute all the database operations within the function f in a single transaction.
|
* Execute all the database operations within the function f in a single transaction.
|
||||||
*/
|
*/
|
||||||
export function transactional<T>(f: () => Promise<T>) {
|
export async function transactional<T>(f: () => Promise<T>): Promise<void> {
|
||||||
entityManager?.transactional(f);
|
await entityManager?.transactional(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R {
|
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R {
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
import { User } from '../../entities/users/user.entity.js';
|
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
// Import { UserRepository } from './user-repository.js';
|
|
||||||
|
|
||||||
// Export class StudentRepository extends UserRepository<Student> {}
|
|
||||||
|
|
||||||
export class StudentRepository extends DwengoEntityRepository<Student> {
|
export class StudentRepository extends DwengoEntityRepository<Student> {
|
||||||
public findByUsername(username: string): Promise<Student | null> {
|
public async findByUsername(username: string): Promise<Student | null> {
|
||||||
return this.findOne({ username: username });
|
return this.findOne({ username: username });
|
||||||
}
|
}
|
||||||
public deleteByUsername(username: string): Promise<void> {
|
public async deleteByUsername(username: string): Promise<void> {
|
||||||
return this.deleteWhere({ username: username });
|
return this.deleteWhere({ username: username });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { UserRepository } from './user-repository.js';
|
|
||||||
|
|
||||||
export class TeacherRepository extends DwengoEntityRepository<Teacher> {
|
export class TeacherRepository extends DwengoEntityRepository<Teacher> {
|
||||||
public findByUsername(username: string): Promise<Teacher | null> {
|
public async findByUsername(username: string): Promise<Teacher | null> {
|
||||||
return this.findOne({ username: username });
|
return this.findOne({ username: username });
|
||||||
}
|
}
|
||||||
public deleteByUsername(username: string): Promise<void> {
|
public async deleteByUsername(username: string): Promise<void> {
|
||||||
return this.deleteWhere({ username: username });
|
return this.deleteWhere({ username: username });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { User } from '../../entities/users/user.entity.js';
|
import { User } from '../../entities/users/user.entity.js';
|
||||||
|
|
||||||
export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
|
export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
|
||||||
public findByUsername(username: string): Promise<T | null> {
|
public async findByUsername(username: string): Promise<T | null> {
|
||||||
return this.findOne({ username } as Partial<T>);
|
return this.findOne({ username } as Partial<T>);
|
||||||
}
|
}
|
||||||
public deleteByUsername(username: string): Promise<void> {
|
public async deleteByUsername(username: string): Promise<void> {
|
||||||
return this.deleteWhere({ username } as Partial<T>);
|
return this.deleteWhere({ username } as Partial<T>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
import { Class } from '../classes/class.entity.js';
|
import { Class } from '../classes/class.entity.js';
|
||||||
import { Group } from './group.entity.js';
|
import { Group } from './group.entity.js';
|
||||||
import { Language } from '../content/language.js';
|
import { Language } from '../content/language.js';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Collection, Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
|
import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
|
||||||
import { Assignment } from './assignment.entity.js';
|
import { Assignment } from './assignment.entity.js';
|
||||||
import { Student } from '../users/student.entity.js';
|
import { Student } from '../users/student.entity.js';
|
||||||
import { GroupRepository } from '../../data/assignments/group-repository.js';
|
import { GroupRepository } from '../../data/assignments/group-repository.js';
|
||||||
|
|
|
@ -16,7 +16,7 @@ export class Submission {
|
||||||
learningObjectLanguage!: Language;
|
learningObjectLanguage!: Language;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'numeric' })
|
@PrimaryKey({ type: 'numeric' })
|
||||||
learningObjectVersion: number = 1;
|
learningObjectVersion = 1;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||||
submissionNumber?: number;
|
submissionNumber?: number;
|
||||||
|
|
|
@ -3,6 +3,12 @@ import { Student } from '../users/student.entity.js';
|
||||||
import { Class } from './class.entity.js';
|
import { Class } from './class.entity.js';
|
||||||
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
|
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
|
||||||
|
|
||||||
|
export enum ClassJoinRequestStatus {
|
||||||
|
Open = 'open',
|
||||||
|
Accepted = 'accepted',
|
||||||
|
Declined = 'declined',
|
||||||
|
}
|
||||||
|
|
||||||
@Entity({
|
@Entity({
|
||||||
repository: () => ClassJoinRequestRepository,
|
repository: () => ClassJoinRequestRepository,
|
||||||
})
|
})
|
||||||
|
@ -22,9 +28,3 @@ export class ClassJoinRequest {
|
||||||
@Enum(() => ClassJoinRequestStatus)
|
@Enum(() => ClassJoinRequestStatus)
|
||||||
status!: ClassJoinRequestStatus;
|
status!: ClassJoinRequestStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ClassJoinRequestStatus {
|
|
||||||
Open = 'open',
|
|
||||||
Accepted = 'accepted',
|
|
||||||
Declined = 'declined',
|
|
||||||
}
|
|
||||||
|
|
10
backend/src/entities/content/educational-goal.entity.ts
Normal file
10
backend/src/entities/content/educational-goal.entity.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Embeddable, Property } from '@mikro-orm/core';
|
||||||
|
|
||||||
|
@Embeddable()
|
||||||
|
export class EducationalGoal {
|
||||||
|
@Property({ type: 'string' })
|
||||||
|
source!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'string' })
|
||||||
|
id!: string;
|
||||||
|
}
|
|
@ -5,5 +5,7 @@ export class LearningObjectIdentifier {
|
||||||
public hruid: string,
|
public hruid: string,
|
||||||
public language: Language,
|
public language: Language,
|
||||||
public version: number
|
public version: number
|
||||||
) {}
|
) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,12 @@
|
||||||
import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
import { Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
import { Language } from './language.js';
|
import { Language } from './language.js';
|
||||||
import { Attachment } from './attachment.entity.js';
|
import { Attachment } from './attachment.entity.js';
|
||||||
import { Teacher } from '../users/teacher.entity.js';
|
import { Teacher } from '../users/teacher.entity.js';
|
||||||
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
|
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { LearningObjectRepository } from '../../data/content/learning-object-repository.js';
|
import { LearningObjectRepository } from '../../data/content/learning-object-repository.js';
|
||||||
|
import { EducationalGoal } from './educational-goal.entity.js';
|
||||||
@Embeddable()
|
import { ReturnValue } from './return-value.entity.js';
|
||||||
export class EducationalGoal {
|
|
||||||
@Property({ type: 'string' })
|
|
||||||
source!: string;
|
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
|
||||||
id!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Embeddable()
|
|
||||||
export class ReturnValue {
|
|
||||||
@Property({ type: 'string' })
|
|
||||||
callbackUrl!: string;
|
|
||||||
|
|
||||||
@Property({ type: 'json' })
|
|
||||||
callbackSchema!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity({ repository: () => LearningObjectRepository })
|
@Entity({ repository: () => LearningObjectRepository })
|
||||||
export class LearningObject {
|
export class LearningObject {
|
||||||
|
@ -36,7 +20,7 @@ export class LearningObject {
|
||||||
language!: Language;
|
language!: Language;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'number' })
|
@PrimaryKey({ type: 'number' })
|
||||||
version: number = 1;
|
version = 1;
|
||||||
|
|
||||||
@Property({ type: 'uuid', unique: true })
|
@Property({ type: 'uuid', unique: true })
|
||||||
uuid = v4();
|
uuid = v4();
|
||||||
|
@ -62,7 +46,7 @@ export class LearningObject {
|
||||||
targetAges?: number[] = [];
|
targetAges?: number[] = [];
|
||||||
|
|
||||||
@Property({ type: 'bool' })
|
@Property({ type: 'bool' })
|
||||||
teacherExclusive: boolean = false;
|
teacherExclusive = false;
|
||||||
|
|
||||||
@Property({ type: 'array' })
|
@Property({ type: 'array' })
|
||||||
skosConcepts: string[] = [];
|
skosConcepts: string[] = [];
|
||||||
|
@ -74,10 +58,10 @@ export class LearningObject {
|
||||||
educationalGoals: EducationalGoal[] = [];
|
educationalGoals: EducationalGoal[] = [];
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
copyright: string = '';
|
copyright = '';
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
license: string = '';
|
license = '';
|
||||||
|
|
||||||
@Property({ type: 'smallint', nullable: true })
|
@Property({ type: 'smallint', nullable: true })
|
||||||
difficulty?: number;
|
difficulty?: number;
|
||||||
|
@ -91,7 +75,7 @@ export class LearningObject {
|
||||||
returnValue!: ReturnValue;
|
returnValue!: ReturnValue;
|
||||||
|
|
||||||
@Property({ type: 'bool' })
|
@Property({ type: 'bool' })
|
||||||
available: boolean = true;
|
available = true;
|
||||||
|
|
||||||
@Property({ type: 'string', nullable: true })
|
@Property({ type: 'string', nullable: true })
|
||||||
contentLocation?: string;
|
contentLocation?: string;
|
||||||
|
|
10
backend/src/entities/content/return-value.entity.ts
Normal file
10
backend/src/entities/content/return-value.entity.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Embeddable, Property } from '@mikro-orm/core';
|
||||||
|
|
||||||
|
@Embeddable()
|
||||||
|
export class ReturnValue {
|
||||||
|
@Property({ type: 'string' })
|
||||||
|
callbackUrl!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'json' })
|
||||||
|
callbackSchema!: string;
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ export class Question {
|
||||||
learningObjectLanguage!: Language;
|
learningObjectLanguage!: Language;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'number' })
|
@PrimaryKey({ type: 'number' })
|
||||||
learningObjectVersion: number = 1;
|
learningObjectVersion = 1;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||||
sequenceNumber?: number;
|
sequenceNumber?: number;
|
||||||
|
|
|
@ -13,12 +13,4 @@ export class Student extends User {
|
||||||
|
|
||||||
@ManyToMany(() => Group)
|
@ManyToMany(() => Group)
|
||||||
groups!: Collection<Group>;
|
groups!: Collection<Group>;
|
||||||
|
|
||||||
constructor(
|
|
||||||
public username: string,
|
|
||||||
public firstName: string,
|
|
||||||
public lastName: string
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,4 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js';
|
||||||
export class Teacher extends User {
|
export class Teacher extends User {
|
||||||
@ManyToMany(() => Class)
|
@ManyToMany(() => Class)
|
||||||
classes!: Collection<Class>;
|
classes!: Collection<Class>;
|
||||||
|
|
||||||
constructor(
|
|
||||||
public username: string,
|
|
||||||
public firstName: string,
|
|
||||||
public lastName: string
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ export abstract class User {
|
||||||
username!: string;
|
username!: string;
|
||||||
|
|
||||||
@Property()
|
@Property()
|
||||||
firstName: string = '';
|
firstName = '';
|
||||||
|
|
||||||
@Property()
|
@Property()
|
||||||
lastName: string = '';
|
lastName = '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
/**
|
|
||||||
* Exception for HTTP 400 Bad Request
|
|
||||||
*/
|
|
||||||
export class BadRequestException extends Error {
|
|
||||||
public status = 400;
|
|
||||||
|
|
||||||
constructor(error: string) {
|
|
||||||
super(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception for HTTP 401 Unauthorized
|
|
||||||
*/
|
|
||||||
export class UnauthorizedException extends Error {
|
|
||||||
status = 401;
|
|
||||||
constructor(message: string = 'Unauthorized') {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception for HTTP 403 Forbidden
|
|
||||||
*/
|
|
||||||
export class ForbiddenException extends Error {
|
|
||||||
status = 403;
|
|
||||||
|
|
||||||
constructor(message: string = 'Forbidden') {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception for HTTP 404 Not Found
|
|
||||||
*/
|
|
||||||
export class NotFoundException extends Error {
|
|
||||||
public status = 404;
|
|
||||||
|
|
||||||
constructor(error: string) {
|
|
||||||
super(error);
|
|
||||||
}
|
|
||||||
}
|
|
10
backend/src/exceptions/bad-request-exception.ts
Normal file
10
backend/src/exceptions/bad-request-exception.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 400 Bad Request
|
||||||
|
*/
|
||||||
|
export class BadRequestException extends ExceptionWithHttpState {
|
||||||
|
constructor(error: string) {
|
||||||
|
super(400, error);
|
||||||
|
}
|
||||||
|
}
|
12
backend/src/exceptions/conflict-exception.ts
Normal file
12
backend/src/exceptions/conflict-exception.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 409 Conflict
|
||||||
|
*/
|
||||||
|
export class ConflictException extends ExceptionWithHttpState {
|
||||||
|
public status = 409;
|
||||||
|
|
||||||
|
constructor(error: string) {
|
||||||
|
super(409, error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ConflictException } from './conflict-exception.js';
|
||||||
|
|
||||||
|
export class EntityAlreadyExistsException extends ConflictException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
11
backend/src/exceptions/exception-with-http-state.ts
Normal file
11
backend/src/exceptions/exception-with-http-state.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
* Exceptions which are associated with a HTTP error code.
|
||||||
|
*/
|
||||||
|
export abstract class ExceptionWithHttpState extends Error {
|
||||||
|
constructor(
|
||||||
|
public status: number,
|
||||||
|
public error: string
|
||||||
|
) {
|
||||||
|
super(error);
|
||||||
|
}
|
||||||
|
}
|
12
backend/src/exceptions/forbidden-exception.ts
Normal file
12
backend/src/exceptions/forbidden-exception.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 403 Forbidden
|
||||||
|
*/
|
||||||
|
export class ForbiddenException extends ExceptionWithHttpState {
|
||||||
|
status = 403;
|
||||||
|
|
||||||
|
constructor(message = 'Forbidden') {
|
||||||
|
super(403, message);
|
||||||
|
}
|
||||||
|
}
|
12
backend/src/exceptions/not-found-exception.ts
Normal file
12
backend/src/exceptions/not-found-exception.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 404 Not Found
|
||||||
|
*/
|
||||||
|
export class NotFoundException extends ExceptionWithHttpState {
|
||||||
|
public status = 404;
|
||||||
|
|
||||||
|
constructor(error: string) {
|
||||||
|
super(404, error);
|
||||||
|
}
|
||||||
|
}
|
10
backend/src/exceptions/unauthorized-exception.ts
Normal file
10
backend/src/exceptions/unauthorized-exception.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 401 Unauthorized
|
||||||
|
*/
|
||||||
|
export class UnauthorizedException extends ExceptionWithHttpState {
|
||||||
|
constructor(message = 'Unauthorized') {
|
||||||
|
super(401, message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,8 @@ import { FALLBACK_LANG } from '../config.js';
|
||||||
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
||||||
import { Class } from '../entities/classes/class.entity.js';
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
import { languageMap } from '../entities/content/language.js';
|
import { languageMap } from '../entities/content/language.js';
|
||||||
import { GroupDTO, mapToGroupDTO } from './group.js';
|
import { GroupDTO } from './group.js';
|
||||||
|
import { getLogger } from '../logging/initalize.js';
|
||||||
|
|
||||||
export interface AssignmentDTO {
|
export interface AssignmentDTO {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -46,7 +47,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
|
||||||
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
|
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
|
||||||
assignment.within = cls;
|
assignment.within = cls;
|
||||||
|
|
||||||
console.log(assignment);
|
getLogger().debug(assignment);
|
||||||
|
|
||||||
return assignment;
|
return assignment;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,6 @@ export interface ClassDTO {
|
||||||
teachers: string[];
|
teachers: string[];
|
||||||
students: string[];
|
students: string[];
|
||||||
joinRequests: string[];
|
joinRequests: string[];
|
||||||
endpoints?: {
|
|
||||||
self: string;
|
|
||||||
invitations: string;
|
|
||||||
assignments: string;
|
|
||||||
students: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToClassDTO(cls: Class): ClassDTO {
|
export function mapToClassDTO(cls: Class): ClassDTO {
|
||||||
|
|
|
@ -58,7 +58,7 @@ export interface EducationalGoal {
|
||||||
|
|
||||||
export interface ReturnValue {
|
export interface ReturnValue {
|
||||||
callback_url: string;
|
callback_url: string;
|
||||||
callback_schema: Record<string, any>;
|
callback_schema: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LearningObjectMetadata {
|
export interface LearningObjectMetadata {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { Question } from '../entities/questions/question.entity.js';
|
import { Question } from '../entities/questions/question.entity.js';
|
||||||
import { UserDTO } from './user.js';
|
|
||||||
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||||
import { mapToStudentDTO, StudentDTO } from './student.js';
|
import { mapToStudentDTO, StudentDTO } from './student.js';
|
||||||
import { TeacherDTO } from './teacher.js';
|
|
||||||
|
|
||||||
export interface QuestionDTO {
|
export interface QuestionDTO {
|
||||||
learningObjectIdentifier: LearningObjectIdentifier;
|
learningObjectIdentifier: LearningObjectIdentifier;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Student } from '../entities/users/student.entity.js';
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
|
import { getStudentRepository } from '../data/repositories.js';
|
||||||
|
|
||||||
export interface StudentDTO {
|
export interface StudentDTO {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -23,7 +24,9 @@ export function mapToStudentDTO(student: Student): StudentDTO {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToStudent(studentData: StudentDTO): Student {
|
export function mapToStudent(studentData: StudentDTO): Student {
|
||||||
const student = new Student(studentData.username, studentData.firstName, studentData.lastName);
|
return getStudentRepository().create({
|
||||||
|
username: studentData.username,
|
||||||
return student;
|
firstName: studentData.firstName,
|
||||||
|
lastName: studentData.lastName,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,10 @@ import { Submission } from '../entities/assignments/submission.entity.js';
|
||||||
import { Language } from '../entities/content/language.js';
|
import { Language } from '../entities/content/language.js';
|
||||||
import { GroupDTO, mapToGroupDTO } from './group.js';
|
import { GroupDTO, mapToGroupDTO } from './group.js';
|
||||||
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js';
|
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js';
|
||||||
import { mapToUser } from './user';
|
import { LearningObjectIdentifier } from './learning-content.js';
|
||||||
import { Student } from '../entities/users/student.entity';
|
|
||||||
|
|
||||||
export interface SubmissionDTO {
|
export interface SubmissionDTO {
|
||||||
learningObjectHruid: string;
|
learningObjectIdentifier: LearningObjectIdentifier;
|
||||||
learningObjectLanguage: Language;
|
|
||||||
learningObjectVersion: number;
|
|
||||||
|
|
||||||
submissionNumber?: number;
|
submissionNumber?: number;
|
||||||
submitter: StudentDTO;
|
submitter: StudentDTO;
|
||||||
|
@ -17,11 +14,21 @@ export interface SubmissionDTO {
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SubmissionDTOId {
|
||||||
|
learningObjectHruid: string;
|
||||||
|
learningObjectLanguage: Language;
|
||||||
|
learningObjectVersion: number;
|
||||||
|
|
||||||
|
submissionNumber?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
||||||
return {
|
return {
|
||||||
learningObjectHruid: submission.learningObjectHruid,
|
learningObjectIdentifier: {
|
||||||
learningObjectLanguage: submission.learningObjectLanguage,
|
hruid: submission.learningObjectHruid,
|
||||||
learningObjectVersion: submission.learningObjectVersion,
|
language: submission.learningObjectLanguage,
|
||||||
|
version: submission.learningObjectVersion,
|
||||||
|
},
|
||||||
|
|
||||||
submissionNumber: submission.submissionNumber,
|
submissionNumber: submission.submissionNumber,
|
||||||
submitter: mapToStudentDTO(submission.submitter),
|
submitter: mapToStudentDTO(submission.submitter),
|
||||||
|
@ -31,11 +38,21 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId {
|
||||||
|
return {
|
||||||
|
learningObjectHruid: submission.learningObjectHruid,
|
||||||
|
learningObjectLanguage: submission.learningObjectLanguage,
|
||||||
|
learningObjectVersion: submission.learningObjectVersion,
|
||||||
|
|
||||||
|
submissionNumber: submission.submissionNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
|
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
|
||||||
const submission = new Submission();
|
const submission = new Submission();
|
||||||
submission.learningObjectHruid = submissionDTO.learningObjectHruid;
|
submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid;
|
||||||
submission.learningObjectLanguage = submissionDTO.learningObjectLanguage;
|
submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language;
|
||||||
submission.learningObjectVersion = submissionDTO.learningObjectVersion;
|
submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!;
|
||||||
// Submission.submissionNumber = submissionDTO.submissionNumber;
|
// Submission.submissionNumber = submissionDTO.submissionNumber;
|
||||||
submission.submitter = mapToStudent(submissionDTO.submitter);
|
submission.submitter = mapToStudent(submissionDTO.submitter);
|
||||||
// Submission.submissionTime = submissionDTO.time;
|
// Submission.submissionTime = submissionDTO.time;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
|
import { getTeacherRepository } from '../data/repositories.js';
|
||||||
|
|
||||||
export interface TeacherDTO {
|
export interface TeacherDTO {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -22,8 +23,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToTeacher(TeacherData: TeacherDTO): Teacher {
|
export function mapToTeacher(teacherData: TeacherDTO): Teacher {
|
||||||
const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName);
|
return getTeacherRepository().create({
|
||||||
|
username: teacherData.username,
|
||||||
return teacher;
|
firstName: teacherData.firstName,
|
||||||
|
lastName: teacherData.lastName,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
|
import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
|
||||||
import LokiTransport from 'winston-loki';
|
import LokiTransport from 'winston-loki';
|
||||||
import { LokiLabels } from 'loki-logger-ts';
|
import { LokiLabels } from 'loki-logger-ts';
|
||||||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||||
|
|
||||||
export class Logger extends WinstonLogger {
|
export class Logger extends WinstonLogger {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -9,7 +9,7 @@ export class Logger extends WinstonLogger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Labels: LokiLabels = {
|
const lokiLabels: LokiLabels = {
|
||||||
source: 'Dwengo-Backend',
|
source: 'Dwengo-Backend',
|
||||||
service: 'API',
|
service: 'API',
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
|
@ -22,28 +22,28 @@ function initializeLogger(): Logger {
|
||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logLevel = getEnvVar(EnvVars.LogLevel);
|
const logLevel = getEnvVar(envVars.LogLevel);
|
||||||
|
|
||||||
const consoleTransport = new transports.Console({
|
const consoleTransport = new transports.Console({
|
||||||
level: getEnvVar(EnvVars.LogLevel),
|
level: getEnvVar(envVars.LogLevel),
|
||||||
format: format.combine(format.cli(), format.colorize()),
|
format: format.combine(format.cli(), format.colorize()),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (getEnvVar(EnvVars.RunMode) === 'dev') {
|
if (getEnvVar(envVars.RunMode) === 'dev') {
|
||||||
return createLogger({
|
return createLogger({
|
||||||
transports: [consoleTransport],
|
transports: [consoleTransport],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const lokiHost = getEnvVar(EnvVars.LokiHost);
|
const lokiHost = getEnvVar(envVars.LokiHost);
|
||||||
|
|
||||||
const lokiTransport: LokiTransport = new LokiTransport({
|
const lokiTransport: LokiTransport = new LokiTransport({
|
||||||
host: lokiHost,
|
host: lokiHost,
|
||||||
labels: Labels,
|
labels: lokiLabels,
|
||||||
level: logLevel,
|
level: logLevel,
|
||||||
json: true,
|
json: true,
|
||||||
format: format.combine(format.timestamp(), format.json()),
|
format: format.combine(format.timestamp(), format.json()),
|
||||||
onConnectionError: (err) => {
|
onConnectionError: (err): void => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(`Connection error: ${err}`);
|
console.error(`Connection error: ${err}`);
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts';
|
||||||
export class MikroOrmLogger extends DefaultLogger {
|
export class MikroOrmLogger extends DefaultLogger {
|
||||||
private logger: Logger = getLogger();
|
private logger: Logger = getLogger();
|
||||||
|
|
||||||
log(namespace: LoggerNamespace, message: string, context?: LogContext) {
|
static createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown {
|
||||||
|
const labels: LokiLabels = {
|
||||||
|
service: 'ORM',
|
||||||
|
};
|
||||||
|
|
||||||
|
let message: string;
|
||||||
|
if (context?.label) {
|
||||||
|
message = `[${namespace}] (${context.label}) ${messageArg}`;
|
||||||
|
} else {
|
||||||
|
message = `[${namespace}] ${messageArg}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: message,
|
||||||
|
labels: labels,
|
||||||
|
context: context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log(namespace: LoggerNamespace, message: string, context?: LogContext): void {
|
||||||
if (!this.isEnabled(namespace, context)) {
|
if (!this.isEnabled(namespace, context)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (namespace) {
|
switch (namespace) {
|
||||||
case 'query':
|
case 'query':
|
||||||
this.logger.debug(this.createMessage(namespace, message, context));
|
this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'query-params':
|
case 'query-params':
|
||||||
// TODO Which log level should this be?
|
// TODO Which log level should this be?
|
||||||
this.logger.info(this.createMessage(namespace, message, context));
|
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'schema':
|
case 'schema':
|
||||||
this.logger.info(this.createMessage(namespace, message, context));
|
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'discovery':
|
case 'discovery':
|
||||||
this.logger.debug(this.createMessage(namespace, message, context));
|
this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'info':
|
case 'info':
|
||||||
this.logger.info(this.createMessage(namespace, message, context));
|
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'deprecated':
|
case 'deprecated':
|
||||||
this.logger.warn(this.createMessage(namespace, message, context));
|
this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
switch (context?.level) {
|
switch (context?.level) {
|
||||||
case 'info':
|
case 'info':
|
||||||
this.logger.info(this.createMessage(namespace, message, context));
|
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'warning':
|
case 'warning':
|
||||||
this.logger.warn(message);
|
this.logger.warn(message);
|
||||||
|
@ -47,23 +66,4 @@ export class MikroOrmLogger extends DefaultLogger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) {
|
|
||||||
const labels: LokiLabels = {
|
|
||||||
service: 'ORM',
|
|
||||||
};
|
|
||||||
|
|
||||||
let message: string;
|
|
||||||
if (context?.label) {
|
|
||||||
message = `[${namespace}] (${context?.label}) ${messageArg}`;
|
|
||||||
} else {
|
|
||||||
message = `[${namespace}] ${messageArg}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: message,
|
|
||||||
labels: labels,
|
|
||||||
context: context,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { getLogger, Logger } from './initalize.js';
|
import { getLogger, Logger } from './initalize.js';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
export function responseTimeLogger(req: Request, res: Response, time: number) {
|
export function responseTimeLogger(req: Request, res: Response, time: number): void {
|
||||||
const logger: Logger = getLogger();
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
const method = req.method;
|
const method = req.method;
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { EnvVars, getEnvVar } from '../../util/envvars.js';
|
import { envVars, getEnvVar } from '../../util/envVars.js';
|
||||||
import { expressjwt } from 'express-jwt';
|
import { expressjwt } from 'express-jwt';
|
||||||
|
import * as jwt from 'jsonwebtoken';
|
||||||
import { JwtPayload } from 'jsonwebtoken';
|
import { JwtPayload } from 'jsonwebtoken';
|
||||||
import jwksClient from 'jwks-rsa';
|
import jwksClient from 'jwks-rsa';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as jwt from 'jsonwebtoken';
|
|
||||||
import { AuthenticatedRequest } from './authenticated-request.js';
|
import { AuthenticatedRequest } from './authenticated-request.js';
|
||||||
import { AuthenticationInfo } from './authentication-info.js';
|
import { AuthenticationInfo } from './authentication-info.js';
|
||||||
import { ForbiddenException, UnauthorizedException } from '../../exceptions.js';
|
import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js';
|
||||||
|
import { ForbiddenException } from '../../exceptions/forbidden-exception.js';
|
||||||
|
|
||||||
const JWKS_CACHE = true;
|
const JWKS_CACHE = true;
|
||||||
const JWKS_RATE_LIMIT = true;
|
const JWKS_RATE_LIMIT = true;
|
||||||
|
@ -32,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient {
|
||||||
|
|
||||||
const idpConfigs = {
|
const idpConfigs = {
|
||||||
student: {
|
student: {
|
||||||
issuer: getEnvVar(EnvVars.IdpStudentUrl),
|
issuer: getEnvVar(envVars.IdpStudentUrl),
|
||||||
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)),
|
jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)),
|
||||||
},
|
},
|
||||||
teacher: {
|
teacher: {
|
||||||
issuer: getEnvVar(EnvVars.IdpTeacherUrl),
|
issuer: getEnvVar(envVars.IdpTeacherUrl),
|
||||||
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)),
|
jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@ const verifyJwtToken = expressjwt({
|
||||||
}
|
}
|
||||||
return signingKey.getPublicKey();
|
return signingKey.getPublicKey();
|
||||||
},
|
},
|
||||||
audience: getEnvVar(EnvVars.IdpAudience),
|
audience: getEnvVar(envVars.IdpAudience),
|
||||||
algorithms: [JWT_ALGORITHM],
|
algorithms: [JWT_ALGORITHM],
|
||||||
credentialsRequired: false,
|
credentialsRequired: false,
|
||||||
requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD,
|
requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD,
|
||||||
|
@ -74,7 +75,7 @@ const verifyJwtToken = expressjwt({
|
||||||
*/
|
*/
|
||||||
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
|
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
|
||||||
if (!req.jwtPayload) {
|
if (!req.jwtPayload) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
const issuer = req.jwtPayload.iss;
|
const issuer = req.jwtPayload.iss;
|
||||||
let accountType: 'student' | 'teacher';
|
let accountType: 'student' | 'teacher';
|
||||||
|
@ -84,8 +85,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
|
||||||
} else if (issuer === idpConfigs.teacher.issuer) {
|
} else if (issuer === idpConfigs.teacher.issuer) {
|
||||||
accountType = 'teacher';
|
accountType = 'teacher';
|
||||||
} else {
|
} else {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accountType: accountType,
|
accountType: accountType,
|
||||||
username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!,
|
username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!,
|
||||||
|
@ -100,10 +102,10 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
|
||||||
* Add the AuthenticationInfo object with the information about the current authentication to the request in order
|
* Add the AuthenticationInfo object with the information about the current authentication to the request in order
|
||||||
* to avoid that the routers have to deal with the JWT token.
|
* to avoid that the routers have to deal with the JWT token.
|
||||||
*/
|
*/
|
||||||
const addAuthenticationInfo = (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => {
|
function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void {
|
||||||
req.auth = getAuthenticationInfo(req);
|
req.auth = getAuthenticationInfo(req);
|
||||||
next();
|
next();
|
||||||
};
|
}
|
||||||
|
|
||||||
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
||||||
|
|
||||||
|
@ -113,9 +115,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
||||||
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
|
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
|
||||||
* to true.
|
* to true.
|
||||||
*/
|
*/
|
||||||
export const authorize =
|
export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) {
|
||||||
(accessCondition: (auth: AuthenticationInfo) => boolean) =>
|
return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
|
||||||
(req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => {
|
|
||||||
if (!req.auth) {
|
if (!req.auth) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
} else if (!accessCondition(req.auth)) {
|
} else if (!accessCondition(req.auth)) {
|
||||||
|
@ -124,6 +125,7 @@ export const authorize =
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware which rejects all unauthenticated users, but accepts all authenticated users.
|
* Middleware which rejects all unauthenticated users, but accepts all authenticated users.
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* Object with information about the user who is currently logged in.
|
* Object with information about the user who is currently logged in.
|
||||||
*/
|
*/
|
||||||
export type AuthenticationInfo = {
|
export interface AuthenticationInfo {
|
||||||
accountType: 'student' | 'teacher';
|
accountType: 'student' | 'teacher';
|
||||||
username: string;
|
username: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||||
|
|
||||||
export default cors({
|
export default cors({
|
||||||
origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','),
|
origin: getEnvVar(envVars.CorsAllowedOrigins).split(','),
|
||||||
allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','),
|
allowedHeaders: getEnvVar(envVars.CorsAllowedHeaders).split(','),
|
||||||
});
|
});
|
||||||
|
|
15
backend/src/middleware/error-handling/error-handler.ts
Normal file
15
backend/src/middleware/error-handling/error-handler.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
import { getLogger, Logger } from '../../logging/initalize.js';
|
||||||
|
import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-state.js';
|
||||||
|
|
||||||
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
|
export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void {
|
||||||
|
if (err instanceof ExceptionWithHttpState) {
|
||||||
|
logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`);
|
||||||
|
res.status(err.status).json(err);
|
||||||
|
} else {
|
||||||
|
logger.error(`Unexpected error occurred while handing a request: ${JSON.stringify(err)}`);
|
||||||
|
res.status(500).json(err);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { LoggerOptions, Options } from '@mikro-orm/core';
|
import { LoggerOptions, Options } from '@mikro-orm/core';
|
||||||
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
|
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
|
||||||
import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js';
|
import { envVars, getEnvVar, getNumericEnvVar } from './util/envVars.js';
|
||||||
import { SqliteDriver } from '@mikro-orm/sqlite';
|
import { SqliteDriver } from '@mikro-orm/sqlite';
|
||||||
import { MikroOrmLogger } from './logging/mikroOrmLogger.js';
|
import { MikroOrmLogger } from './logging/mikroOrmLogger.js';
|
||||||
|
|
||||||
|
@ -42,33 +42,35 @@ const entities = [
|
||||||
Question,
|
Question,
|
||||||
];
|
];
|
||||||
|
|
||||||
function config(testingMode: boolean = false): Options {
|
function config(testingMode = false): Options {
|
||||||
if (testingMode) {
|
if (testingMode) {
|
||||||
return {
|
return {
|
||||||
driver: SqliteDriver,
|
driver: SqliteDriver,
|
||||||
dbName: getEnvVar(EnvVars.DbName),
|
dbName: getEnvVar(envVars.DbName),
|
||||||
subscribers: [new SqliteAutoincrementSubscriber()],
|
subscribers: [new SqliteAutoincrementSubscriber()],
|
||||||
entities: entities,
|
entities: entities,
|
||||||
|
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
|
||||||
// EntitiesTs: entitiesTs,
|
// EntitiesTs: entitiesTs,
|
||||||
|
|
||||||
// Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
|
// Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
|
||||||
// (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
|
// (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
|
||||||
dynamicImportProvider: (id) => import(id),
|
dynamicImportProvider: async (id) => import(id),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
driver: PostgreSqlDriver,
|
driver: PostgreSqlDriver,
|
||||||
host: getEnvVar(EnvVars.DbHost),
|
host: getEnvVar(envVars.DbHost),
|
||||||
port: getNumericEnvVar(EnvVars.DbPort),
|
port: getNumericEnvVar(envVars.DbPort),
|
||||||
dbName: getEnvVar(EnvVars.DbName),
|
dbName: getEnvVar(envVars.DbName),
|
||||||
user: getEnvVar(EnvVars.DbUsername),
|
user: getEnvVar(envVars.DbUsername),
|
||||||
password: getEnvVar(EnvVars.DbPassword),
|
password: getEnvVar(envVars.DbPassword),
|
||||||
entities: entities,
|
entities: entities,
|
||||||
|
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
|
||||||
// EntitiesTs: entitiesTs,
|
// EntitiesTs: entitiesTs,
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
debug: getEnvVar(EnvVars.LogLevel) === 'debug',
|
debug: getEnvVar(envVars.LogLevel) === 'debug',
|
||||||
loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
|
loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { EntityManager, MikroORM } from '@mikro-orm/core';
|
import { EntityManager, MikroORM } from '@mikro-orm/core';
|
||||||
import config from './mikro-orm.config.js';
|
import config from './mikro-orm.config.js';
|
||||||
import { EnvVars, getEnvVar } from './util/envvars.js';
|
import { envVars, getEnvVar } from './util/envVars.js';
|
||||||
import { getLogger, Logger } from './logging/initalize.js';
|
import { getLogger, Logger } from './logging/initalize.js';
|
||||||
|
|
||||||
let orm: MikroORM | undefined;
|
let orm: MikroORM | undefined;
|
||||||
export async function initORM(testingMode: boolean = false) {
|
export async function initORM(testingMode = false): Promise<void> {
|
||||||
const logger: Logger = getLogger();
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
logger.info('Initializing ORM');
|
logger.info('Initializing ORM');
|
||||||
|
@ -12,7 +12,7 @@ export async function initORM(testingMode: boolean = false) {
|
||||||
|
|
||||||
orm = await MikroORM.init(config(testingMode));
|
orm = await MikroORM.init(config(testingMode));
|
||||||
// Update the database scheme if necessary and enabled.
|
// Update the database scheme if necessary and enabled.
|
||||||
if (getEnvVar(EnvVars.DbUpdate)) {
|
if (getEnvVar(envVars.DbUpdate)) {
|
||||||
await orm.schema.updateSchema();
|
await orm.schema.updateSchema();
|
||||||
} else {
|
} else {
|
||||||
const diff = await orm.schema.getUpdateSchemaSQL();
|
const diff = await orm.schema.getUpdateSchemaSQL();
|
||||||
|
|
|
@ -19,7 +19,7 @@ router.get('/:id', getAssignmentHandler);
|
||||||
|
|
||||||
router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
|
router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
|
||||||
|
|
||||||
router.get('/:id/questions', (req, res) => {
|
router.get('/:id/questions', (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
questions: ['0'],
|
questions: ['0'],
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,21 +4,21 @@ import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/aut
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Returns auth configuration for frontend
|
// Returns auth configuration for frontend
|
||||||
router.get('/config', (req, res) => {
|
router.get('/config', (_req, res) => {
|
||||||
res.json(getFrontendAuthConfig());
|
res.json(getFrontendAuthConfig());
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/testAuthenticatedOnly', authenticatedOnly, (req, res) => {
|
router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => {
|
||||||
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
|
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
|
||||||
res.json({ message: 'If you see this, you should be authenticated!' });
|
res.json({ message: 'If you see this, you should be authenticated!' });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/testStudentsOnly', studentsOnly, (req, res) => {
|
router.get('/testStudentsOnly', studentsOnly, (_req, res) => {
|
||||||
/* #swagger.security = [{ "student": [ ] }] */
|
/* #swagger.security = [{ "student": [ ] }] */
|
||||||
res.json({ message: 'If you see this, you should be a student!' });
|
res.json({ message: 'If you see this, you should be a student!' });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/testTeachersOnly', teachersOnly, (req, res) => {
|
router.get('/testTeachersOnly', teachersOnly, (_req, res) => {
|
||||||
/* #swagger.security = [{ "teacher": [ ] }] */
|
/* #swagger.security = [{ "teacher": [ ] }] */
|
||||||
res.json({ message: 'If you see this, you should be a teacher!' });
|
res.json({ message: 'If you see this, you should be a teacher!' });
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ router.get('/:groupid', getGroupHandler);
|
||||||
router.get('/:groupid', getGroupSubmissionsHandler);
|
router.get('/:groupid', getGroupSubmissionsHandler);
|
||||||
|
|
||||||
// The list of questions a group has made
|
// The list of questions a group has made
|
||||||
router.get('/:id/questions', (req, res) => {
|
router.get('/:id/questions', (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
questions: ['0'],
|
questions: ['0'],
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { Response, Router } from 'express';
|
import { Response, Router } from 'express';
|
||||||
import studentRouter from './students.js';
|
import studentRouter from './students.js';
|
||||||
import groupRouter from './groups.js';
|
import teacherRouter from './teachers.js';
|
||||||
import assignmentRouter from './assignments.js';
|
|
||||||
import submissionRouter from './submissions.js';
|
|
||||||
import classRouter from './classes.js';
|
import classRouter from './classes.js';
|
||||||
import questionRouter from './questions.js';
|
|
||||||
import authRouter from './auth.js';
|
import authRouter from './auth.js';
|
||||||
import themeRoutes from './themes.js';
|
import themeRoutes from './themes.js';
|
||||||
import learningPathRoutes from './learning-paths.js';
|
import learningPathRoutes from './learning-paths.js';
|
||||||
|
@ -22,11 +19,8 @@ router.get('/', (_, res: Response) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
|
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
|
||||||
router.use('/group', groupRouter /* #swagger.tags = ['Group'] */);
|
router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */);
|
||||||
router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */);
|
|
||||||
router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */);
|
|
||||||
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
|
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
|
||||||
router.use('/question', questionRouter /* #swagger.tags = ['Question'] */);
|
|
||||||
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
|
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
|
||||||
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
|
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
|
||||||
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);
|
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
getStudentHandler,
|
getStudentHandler,
|
||||||
getStudentSubmissionsHandler,
|
getStudentSubmissionsHandler,
|
||||||
} from '../controllers/students.js';
|
} from '../controllers/students.js';
|
||||||
import { getStudentGroups } from '../services/students.js';
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
// Root endpoint used to search objects
|
||||||
|
@ -37,7 +37,7 @@ router.get('/:id/assignments', getStudentAssignmentsHandler);
|
||||||
router.get('/:id/groups', getStudentGroupsHandler);
|
router.get('/:id/groups', getStudentGroupsHandler);
|
||||||
|
|
||||||
// A list of questions a user has created
|
// A list of questions a user has created
|
||||||
router.get('/:id/questions', (req, res) => {
|
router.get('/:id/questions', (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
questions: ['0'],
|
questions: ['0'],
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
// Root endpoint used to search objects
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
submissions: ['0', '1'],
|
submissions: ['0', '1'],
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,7 +28,7 @@ router.get('/:username/students', getTeacherStudentHandler);
|
||||||
router.get('/:username/questions', getTeacherQuestionHandler);
|
router.get('/:username/questions', getTeacherQuestionHandler);
|
||||||
|
|
||||||
// Invitations to other classes a teacher received
|
// Invitations to other classes a teacher received
|
||||||
router.get('/:id/invitations', (req, res) => {
|
router.get('/:id/invitations', (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
invitations: ['0'],
|
invitations: ['0'],
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
|
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 { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
|
||||||
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
|
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
|
||||||
|
import { getLogger } from '../logging/initalize.js';
|
||||||
|
|
||||||
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
|
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
|
@ -21,7 +21,7 @@ export async function getAllAssignments(classid: string, full: boolean): Promise
|
||||||
return assignments.map(mapToAssignmentDTOId);
|
return assignments.map(mapToAssignmentDTOId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<Assignment | null> {
|
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO | null> {
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
const cls = await classRepository.findById(classid);
|
const cls = await classRepository.findById(classid);
|
||||||
|
|
||||||
|
@ -36,8 +36,9 @@ export async function createAssignment(classid: string, assignmentData: Assignme
|
||||||
const newAssignment = assignmentRepository.create(assignment);
|
const newAssignment = assignmentRepository.create(assignment);
|
||||||
await assignmentRepository.save(newAssignment);
|
await assignmentRepository.save(newAssignment);
|
||||||
|
|
||||||
return newAssignment;
|
return mapToAssignmentDTO(newAssignment);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
getLogger().error(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,7 +61,11 @@ export async function getAssignment(classid: string, id: number): Promise<Assign
|
||||||
return mapToAssignmentDTO(assignment);
|
return mapToAssignmentDTO(assignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAssignmentsSubmissions(classid: string, assignmentNumber: number): Promise<SubmissionDTO[]> {
|
export async function getAssignmentsSubmissions(
|
||||||
|
classid: string,
|
||||||
|
assignmentNumber: number,
|
||||||
|
full: boolean
|
||||||
|
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
const cls = await classRepository.findById(classid);
|
const cls = await classRepository.findById(classid);
|
||||||
|
|
||||||
|
@ -79,7 +84,11 @@ export async function getAssignmentsSubmissions(classid: string, assignmentNumbe
|
||||||
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
|
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
|
||||||
|
|
||||||
const submissionRepository = getSubmissionRepository();
|
const submissionRepository = getSubmissionRepository();
|
||||||
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
|
const submissions = (await Promise.all(groups.map(async (group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
|
||||||
|
|
||||||
return submissions.map(mapToSubmissionDTO);
|
if (full) {
|
||||||
|
return submissions.map(mapToSubmissionDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return submissions.map(mapToSubmissionDTOId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js';
|
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 { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
||||||
import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
|
import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
|
||||||
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js';
|
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js';
|
||||||
|
@ -21,16 +20,18 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[
|
||||||
return classes.map((cls) => cls.classId!);
|
return classes.map((cls) => cls.classId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createClass(classData: ClassDTO): Promise<Class | null> {
|
export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> {
|
||||||
const teacherRepository = getTeacherRepository();
|
const teacherRepository = getTeacherRepository();
|
||||||
const teacherUsernames = classData.teachers || [];
|
const teacherUsernames = classData.teachers || [];
|
||||||
const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher != null);
|
const teachers = (await Promise.all(teacherUsernames.map(async (id) => teacherRepository.findByUsername(id)))).filter(
|
||||||
|
(teacher) => teacher !== null
|
||||||
|
);
|
||||||
|
|
||||||
const studentRepository = getStudentRepository();
|
const studentRepository = getStudentRepository();
|
||||||
const studentUsernames = classData.students || [];
|
const studentUsernames = classData.students || [];
|
||||||
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null);
|
const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter(
|
||||||
|
(student) => student !== null
|
||||||
//Const cls = mapToClass(classData, teachers, students);
|
);
|
||||||
|
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
|
|
||||||
|
@ -42,7 +43,7 @@ export async function createClass(classData: ClassDTO): Promise<Class | null> {
|
||||||
});
|
});
|
||||||
await classRepository.save(newClass);
|
await classRepository.save(newClass);
|
||||||
|
|
||||||
return newClass;
|
return mapToClassDTO(newClass);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
return null;
|
return null;
|
|
@ -1,4 +1,3 @@
|
||||||
import { GroupRepository } from '../data/assignments/group-repository.js';
|
|
||||||
import {
|
import {
|
||||||
getAssignmentRepository,
|
getAssignmentRepository,
|
||||||
getClassRepository,
|
getClassRepository,
|
||||||
|
@ -8,7 +7,8 @@ import {
|
||||||
} from '../data/repositories.js';
|
} from '../data/repositories.js';
|
||||||
import { Group } from '../entities/assignments/group.entity.js';
|
import { Group } from '../entities/assignments/group.entity.js';
|
||||||
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
||||||
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
|
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
|
||||||
|
import { getLogger } from '../logging/initalize.js';
|
||||||
|
|
||||||
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> {
|
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> {
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
|
@ -43,9 +43,11 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
|
||||||
const studentRepository = getStudentRepository();
|
const studentRepository = getStudentRepository();
|
||||||
|
|
||||||
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
|
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);
|
const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter(
|
||||||
|
(student) => student !== null
|
||||||
|
);
|
||||||
|
|
||||||
console.log(members);
|
getLogger().debug(members);
|
||||||
|
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
const cls = await classRepository.findById(classid);
|
const cls = await classRepository.findById(classid);
|
||||||
|
@ -71,7 +73,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
|
||||||
|
|
||||||
return newGroup;
|
return newGroup;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
getLogger().error(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,15 +97,19 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu
|
||||||
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
|
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
|
||||||
|
|
||||||
if (full) {
|
if (full) {
|
||||||
console.log('full');
|
getLogger().debug({ full: full, groups: groups });
|
||||||
console.log(groups);
|
|
||||||
return groups.map(mapToGroupDTO);
|
return groups.map(mapToGroupDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups.map(mapToGroupDTOId);
|
return groups.map(mapToGroupDTOId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise<SubmissionDTO[]> {
|
export async function getGroupSubmissions(
|
||||||
|
classId: string,
|
||||||
|
assignmentNumber: number,
|
||||||
|
groupNumber: number,
|
||||||
|
full: boolean
|
||||||
|
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
const cls = await classRepository.findById(classId);
|
const cls = await classRepository.findById(classId);
|
||||||
|
|
||||||
|
@ -128,5 +134,9 @@ export async function getGroupSubmissions(classId: string, assignmentNumber: num
|
||||||
const submissionRepository = getSubmissionRepository();
|
const submissionRepository = getSubmissionRepository();
|
||||||
const submissions = await submissionRepository.findAllSubmissionsForGroup(group);
|
const submissions = await submissionRepository.findAllSubmissionsForGroup(group);
|
||||||
|
|
||||||
return submissions.map(mapToSubmissionDTO);
|
if (full) {
|
||||||
|
return submissions.map(mapToSubmissionDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return submissions.map(mapToSubmissionDTOId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { DWENGO_API_BASE } from '../config.js';
|
import { DWENGO_API_BASE } from '../config.js';
|
||||||
import { fetchWithLogging } from '../util/api-helper.js';
|
import { fetchWithLogging } from '../util/api-helper.js';
|
||||||
import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js';
|
import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js';
|
||||||
|
import { getLogger } from '../logging/initalize.js';
|
||||||
|
|
||||||
function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
|
function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
|
||||||
return {
|
return {
|
||||||
|
@ -37,7 +38,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`);
|
getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +46,13 @@ export async function getLearningObjectById(hruid: string, language: string): Pr
|
||||||
return filterData(metadata, htmlUrl);
|
return filterData(metadata, htmlUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic function to fetch learning paths
|
||||||
|
*/
|
||||||
|
function fetchLearningPaths(_arg0: string[], _language: string, _arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic function to fetch learning objects (full data or just HRUIDs)
|
* Generic function to fetch learning objects (full data or just HRUIDs)
|
||||||
*/
|
*/
|
||||||
|
@ -53,7 +61,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri
|
||||||
const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`);
|
const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`);
|
||||||
|
|
||||||
if (!learningPathResponse.success || !learningPathResponse.data?.length) {
|
if (!learningPathResponse.success || !learningPathResponse.data?.length) {
|
||||||
console.error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`);
|
getLogger().error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +75,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri
|
||||||
objects.filter((obj): obj is FilteredLearningObject => obj !== null)
|
objects.filter((obj): obj is FilteredLearningObject => obj !== null)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error fetching learning objects:', error);
|
getLogger().error('❌ Error fetching learning objects:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,6 +93,3 @@ export async function getLearningObjectsFromPath(hruid: string, language: string
|
||||||
export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> {
|
export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> {
|
||||||
return (await fetchLearningObjects(hruid, false, language)) as 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.');
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Attachment } from '../../entities/content/attachment.entity.js';
|
||||||
import { LearningObjectIdentifier } from '../../interfaces/learning-content.js';
|
import { LearningObjectIdentifier } from '../../interfaces/learning-content.js';
|
||||||
|
|
||||||
const attachmentService = {
|
const attachmentService = {
|
||||||
getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
|
async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
|
||||||
const attachmentRepo = getAttachmentRepository();
|
const attachmentRepo = getAttachmentRepository();
|
||||||
|
|
||||||
if (learningObjectId.version) {
|
if (learningObjectId.version) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { LearningObjectProvider } from './learning-object-provider.js';
|
import { LearningObjectProvider } from './learning-object-provider.js';
|
||||||
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js';
|
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js';
|
||||||
import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js';
|
import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js';
|
||||||
import { Language } from '../../entities/content/language.js';
|
|
||||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||||
import { getUrlStringForLearningObject } from '../../util/links.js';
|
import { getUrlStringForLearningObject } from '../../util/links.js';
|
||||||
import processingService from './processing/processing-service.js';
|
import processingService from './processing/processing-service.js';
|
||||||
|
@ -41,10 +40,10 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> {
|
async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||||
const learningObjectRepo = getLearningObjectRepository();
|
const learningObjectRepo = getLearningObjectRepository();
|
||||||
|
|
||||||
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language);
|
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,11 +64,11 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
|
||||||
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
|
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
|
||||||
const learningObjectRepo = getLearningObjectRepository();
|
const learningObjectRepo = getLearningObjectRepository();
|
||||||
|
|
||||||
const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language);
|
const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
|
||||||
if (!learningObject) {
|
if (!learningObject) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id));
|
return await processingService.render(learningObject, async (id) => findLearningObjectEntityById(id));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,7 +95,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
|
||||||
throw new NotFoundError('The learning path with the given ID could not be found.');
|
throw new NotFoundError('The learning path with the given ID could not be found.');
|
||||||
}
|
}
|
||||||
const learningObjects = await Promise.all(
|
const learningObjects = await Promise.all(
|
||||||
learningPath.nodes.map((it) => {
|
learningPath.nodes.map(async (it) => {
|
||||||
const learningObject = learningObjectService.getLearningObjectById({
|
const learningObject = learningObjectService.getLearningObjectById({
|
||||||
hruid: it.learningObjectHruid,
|
hruid: it.learningObjectHruid,
|
||||||
language: it.language,
|
language: it.language,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js';
|
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js';
|
||||||
import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js';
|
import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js';
|
||||||
import { LearningObjectProvider } from './learning-object-provider.js';
|
import { LearningObjectProvider } from './learning-object-provider.js';
|
||||||
import { EnvVars, getEnvVar } from '../../util/envvars.js';
|
import { envVars, getEnvVar } from '../../util/envVars.js';
|
||||||
import databaseLearningObjectProvider from './database-learning-object-provider.js';
|
import databaseLearningObjectProvider from './database-learning-object-provider.js';
|
||||||
|
|
||||||
function getProvider(id: LearningObjectIdentifier): LearningObjectProvider {
|
function getProvider(id: LearningObjectIdentifier): LearningObjectProvider {
|
||||||
if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) {
|
if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
|
||||||
return databaseLearningObjectProvider;
|
return databaseLearningObjectProvider;
|
||||||
}
|
}
|
||||||
return dwengoApiLearningObjectProvider;
|
return dwengoApiLearningObjectProvider;
|
||||||
|
@ -18,28 +18,28 @@ const learningObjectService = {
|
||||||
/**
|
/**
|
||||||
* Fetches a single learning object by its HRUID
|
* Fetches a single learning object by its HRUID
|
||||||
*/
|
*/
|
||||||
getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> {
|
async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> {
|
||||||
return getProvider(id).getLearningObjectById(id);
|
return getProvider(id).getLearningObjectById(id);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch full learning object data (metadata)
|
* Fetch full learning object data (metadata)
|
||||||
*/
|
*/
|
||||||
getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> {
|
async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> {
|
||||||
return getProvider(id).getLearningObjectsFromPath(id);
|
return getProvider(id).getLearningObjectsFromPath(id);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch only learning object HRUIDs
|
* Fetch only learning object HRUIDs
|
||||||
*/
|
*/
|
||||||
getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> {
|
async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> {
|
||||||
return getProvider(id).getLearningObjectIdsFromPath(id);
|
return getProvider(id).getLearningObjectIdsFromPath(id);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain a HTML-rendering of the learning object with the given identifier (as a string).
|
* Obtain a HTML-rendering of the learning object with the given identifier (as a string).
|
||||||
*/
|
*/
|
||||||
getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
|
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
|
||||||
return getProvider(id).getLearningObjectHTML(id);
|
return getProvider(id).getLearningObjectHTML(id);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ class AudioProcessor extends StringProcessor {
|
||||||
super(DwengoContentType.AUDIO_MPEG);
|
super(DwengoContentType.AUDIO_MPEG);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderFn(audioUrl: string): string {
|
override renderFn(audioUrl: string): string {
|
||||||
return DOMPurify.sanitize(`<audio controls>
|
return DOMPurify.sanitize(`<audio controls>
|
||||||
<source src="${audioUrl}" type=${type}>
|
<source src="${audioUrl}" type=${type}>
|
||||||
Your browser does not support the audio element.
|
Your browser does not support the audio element.
|
||||||
|
|
|
@ -15,7 +15,7 @@ class ExternProcessor extends StringProcessor {
|
||||||
super(DwengoContentType.EXTERN);
|
super(DwengoContentType.EXTERN);
|
||||||
}
|
}
|
||||||
|
|
||||||
override renderFn(externURL: string) {
|
override renderFn(externURL: string): string {
|
||||||
if (!isValidHttpUrl(externURL)) {
|
if (!isValidHttpUrl(externURL)) {
|
||||||
throw new ProcessingError('The url is not valid: ' + externURL);
|
throw new ProcessingError('The url is not valid: ' + externURL);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ class GiftProcessor extends StringProcessor {
|
||||||
super(DwengoContentType.GIFT);
|
super(DwengoContentType.GIFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
override renderFn(giftString: string) {
|
override renderFn(giftString: string): string {
|
||||||
const quizQuestions: GIFTQuestion[] = parse(giftString);
|
const quizQuestions: GIFTQuestion[] = parse(giftString);
|
||||||
|
|
||||||
let html = "<div class='learning-object-gift'>\n";
|
let html = "<div class='learning-object-gift'>\n";
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Category } from 'gift-pegjs';
|
||||||
import { ProcessingError } from '../../processing-error.js';
|
import { ProcessingError } from '../../processing-error.js';
|
||||||
|
|
||||||
export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> {
|
export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> {
|
||||||
render(question: Category, questionNumber: number): string {
|
override render(_question: Category, _questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'Category' is not supported yet!");
|
throw new ProcessingError("The question type 'Category' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Description } from 'gift-pegjs';
|
||||||
import { ProcessingError } from '../../processing-error.js';
|
import { ProcessingError } from '../../processing-error.js';
|
||||||
|
|
||||||
export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> {
|
export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> {
|
||||||
render(question: Description, questionNumber: number): string {
|
override render(_question: Description, _questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'Description' is not supported yet!");
|
throw new ProcessingError("The question type 'Description' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { GIFTQuestionRenderer } from './gift-question-renderer.js';
|
||||||
import { Essay } from 'gift-pegjs';
|
import { Essay } from 'gift-pegjs';
|
||||||
|
|
||||||
export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> {
|
export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> {
|
||||||
render(question: Essay, questionNumber: number): string {
|
override render(question: Essay, questionNumber: number): string {
|
||||||
let renderedHtml = '';
|
let renderedHtml = '';
|
||||||
if (question.title) {
|
if (question.title) {
|
||||||
renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`;
|
renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Matching } from 'gift-pegjs';
|
||||||
import { ProcessingError } from '../../processing-error.js';
|
import { ProcessingError } from '../../processing-error.js';
|
||||||
|
|
||||||
export class MatchingQuestionRenderer extends GIFTQuestionRenderer<Matching> {
|
export class MatchingQuestionRenderer extends GIFTQuestionRenderer<Matching> {
|
||||||
render(question: Matching, questionNumber: number): string {
|
override render(_question: Matching, _questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'Matching' is not supported yet!");
|
throw new ProcessingError("The question type 'Matching' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { GIFTQuestionRenderer } from './gift-question-renderer.js';
|
||||||
import { MultipleChoice } from 'gift-pegjs';
|
import { MultipleChoice } from 'gift-pegjs';
|
||||||
|
|
||||||
export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<MultipleChoice> {
|
export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<MultipleChoice> {
|
||||||
render(question: MultipleChoice, questionNumber: number): string {
|
override render(question: MultipleChoice, questionNumber: number): string {
|
||||||
let renderedHtml = '';
|
let renderedHtml = '';
|
||||||
if (question.title) {
|
if (question.title) {
|
||||||
renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`;
|
renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Numerical } from 'gift-pegjs';
|
||||||
import { ProcessingError } from '../../processing-error.js';
|
import { ProcessingError } from '../../processing-error.js';
|
||||||
|
|
||||||
export class NumericalQuestionRenderer extends GIFTQuestionRenderer<Numerical> {
|
export class NumericalQuestionRenderer extends GIFTQuestionRenderer<Numerical> {
|
||||||
render(question: Numerical, questionNumber: number): string {
|
override render(_question: Numerical, _questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'Numerical' is not supported yet!");
|
throw new ProcessingError("The question type 'Numerical' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ShortAnswer } from 'gift-pegjs';
|
||||||
import { ProcessingError } from '../../processing-error.js';
|
import { ProcessingError } from '../../processing-error.js';
|
||||||
|
|
||||||
export class ShortQuestionRenderer extends GIFTQuestionRenderer<ShortAnswer> {
|
export class ShortQuestionRenderer extends GIFTQuestionRenderer<ShortAnswer> {
|
||||||
render(question: ShortAnswer, questionNumber: number): string {
|
override render(_question: ShortAnswer, _questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'ShortAnswer' is not supported yet!");
|
throw new ProcessingError("The question type 'ShortAnswer' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { TrueFalse } from 'gift-pegjs';
|
||||||
import { ProcessingError } from '../../processing-error.js';
|
import { ProcessingError } from '../../processing-error.js';
|
||||||
|
|
||||||
export class TrueFalseQuestionRenderer extends GIFTQuestionRenderer<TrueFalse> {
|
export class TrueFalseQuestionRenderer extends GIFTQuestionRenderer<TrueFalse> {
|
||||||
render(question: TrueFalse, questionNumber: number): string {
|
override render(_question: TrueFalse, _questionNumber: number): string {
|
||||||
throw new ProcessingError("The question type 'TrueFalse' is not supported yet!");
|
throw new ProcessingError("The question type 'TrueFalse' is not supported yet!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ class BlockImageProcessor extends InlineImageProcessor {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
override renderFn(imageUrl: string) {
|
override renderFn(imageUrl: string): string {
|
||||||
const inlineHtml = super.render(imageUrl);
|
const inlineHtml = super.render(imageUrl);
|
||||||
return DOMPurify.sanitize(`<div>${inlineHtml}</div>`);
|
return DOMPurify.sanitize(`<div>${inlineHtml}</div>`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ class InlineImageProcessor extends StringProcessor {
|
||||||
super(contentType);
|
super(contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
override renderFn(imageUrl: string) {
|
override renderFn(imageUrl: string): string {
|
||||||
if (!isValidHttpUrl(imageUrl)) {
|
if (!isValidHttpUrl(imageUrl)) {
|
||||||
throw new ProcessingError(`Image URL is invalid: ${imageUrl}`);
|
throw new ProcessingError(`Image URL is invalid: ${imageUrl}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,26 +14,24 @@ class MarkdownProcessor extends StringProcessor {
|
||||||
super(DwengoContentType.TEXT_MARKDOWN);
|
super(DwengoContentType.TEXT_MARKDOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
override renderFn(mdText: string) {
|
static replaceLinks(html: string): string {
|
||||||
let html = '';
|
|
||||||
try {
|
|
||||||
marked.use({ renderer: dwengoMarkedRenderer });
|
|
||||||
html = marked(mdText, { async: false });
|
|
||||||
html = this.replaceLinks(html); // Replace html image links path
|
|
||||||
} catch (e: any) {
|
|
||||||
throw new ProcessingError(e.message);
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceLinks(html: string) {
|
|
||||||
const proc = new InlineImageProcessor();
|
const proc = new InlineImageProcessor();
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
/<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g,
|
/<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g,
|
||||||
(match: string, src: string, alt: string, altText: string, title: string, titleText: string) => proc.render(src)
|
(_match: string, src: string, _alt: string, _altText: string, _title: string, _titleText: string) => proc.render(src)
|
||||||
);
|
);
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override renderFn(mdText: string): string {
|
||||||
|
try {
|
||||||
|
marked.use({ renderer: dwengoMarkedRenderer });
|
||||||
|
const html = marked(mdText, { async: false });
|
||||||
|
return MarkdownProcessor.replaceLinks(html); // Replace html image links path
|
||||||
|
} catch (e: unknown) {
|
||||||
|
throw new ProcessingError('Unknown error while processing markdown: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { MarkdownProcessor };
|
export { MarkdownProcessor };
|
||||||
|
|
|
@ -15,7 +15,7 @@ class PdfProcessor extends StringProcessor {
|
||||||
super(DwengoContentType.APPLICATION_PDF);
|
super(DwengoContentType.APPLICATION_PDF);
|
||||||
}
|
}
|
||||||
|
|
||||||
override renderFn(pdfUrl: string) {
|
override renderFn(pdfUrl: string): string {
|
||||||
if (!isValidHttpUrl(pdfUrl)) {
|
if (!isValidHttpUrl(pdfUrl)) {
|
||||||
throw new ProcessingError(`PDF URL is invalid: ${pdfUrl}`);
|
throw new ProcessingError(`PDF URL is invalid: ${pdfUrl}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" l
|
||||||
const LEARNING_OBJECT_DOES_NOT_EXIST = "<div class='non-existing-learning-object' />";
|
const LEARNING_OBJECT_DOES_NOT_EXIST = "<div class='non-existing-learning-object' />";
|
||||||
|
|
||||||
class ProcessingService {
|
class ProcessingService {
|
||||||
private processors!: Map<DwengoContentType, Processor<any>>;
|
private processors!: Map<DwengoContentType, Processor<DwengoContentType>>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const processors = [
|
const processors = [
|
||||||
|
|
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