Merge branch 'dev' into feat/discussions
This commit is contained in:
commit
edc52a559c
181 changed files with 7820 additions and 1515 deletions
|
@ -13,7 +13,7 @@
|
|||
#DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api
|
||||
# The default fallback language.
|
||||
#DWENGO_FALLBACK_LANGUAGE=nl
|
||||
# Whether running in production mode or not. Possible values are "prod" or "dev".
|
||||
# Whether running in production mode or not. Possible values are "prod", "staging", "test" or "dev".
|
||||
#DWENGO_RUN_MODE=dev
|
||||
|
||||
# ! Change this! The hostname or IP address of the database
|
||||
|
@ -66,3 +66,12 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://hostname/realms/teacher/protocol/openid
|
|||
# The address of the Loki instance, a log aggregation system.
|
||||
# If running your stack in docker, this should use the docker service name.
|
||||
#DWENGO_LOGGING_LOKI_HOST=http://localhost:3102
|
||||
|
||||
# The hostname or IP address of the caching server, e.g. Memcached.
|
||||
# If running your stack in docker, this should use the docker service name.
|
||||
#DWENGO_CACHE_HOST=localhost
|
||||
#DWENGO_CACHE_PORT=11211
|
||||
# The time-to-live (TTL) for cached items in seconds.
|
||||
#DWENGO_CACHE_TTL=3600
|
||||
# If your cache server benefits from a prefix, you can set it here.
|
||||
#DWENGO_CACHE_KEY_PREFIX=dwengo
|
||||
|
|
|
@ -35,3 +35,7 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/op
|
|||
|
||||
DWENGO_LOGGING_LEVEL=info
|
||||
DWENGO_LOGGING_LOKI_HOST=http://logging:3102
|
||||
|
||||
DWENGO_CACHE_HOST=caching
|
||||
#DWENGO_CACHE_PORT=11211
|
||||
DWENGO_CACHE_TTL=604800
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
PORT=3000
|
||||
DWENGO_RUN_MODE=staging
|
||||
|
||||
DWENGO_DB_HOST=db
|
||||
DWENGO_DB_PORT=5432
|
||||
DWENGO_DB_USERNAME=postgres
|
||||
|
@ -18,4 +20,8 @@ DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080
|
|||
|
||||
# Logging and monitoring
|
||||
|
||||
LOKI_HOST=http://logging:3102
|
||||
DWENGO_LOGGING_LEVEL=debug
|
||||
DWENGO_LOGGING_LOKI_HOST=http://logging:3102
|
||||
|
||||
DWENGO_CACHE_HOST=caching
|
||||
DWENGO_CACHE_TTL=86400
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
### Dwengo ###
|
||||
|
||||
DWENGO_PORT=3000
|
||||
DWENGO_RUN_MODE=test
|
||||
|
||||
DWENGO_DB_NAME=":memory:"
|
||||
DWENGO_DB_UPDATE=true
|
||||
|
|
|
@ -27,6 +27,13 @@ FROM node:22 AS production-stage
|
|||
|
||||
WORKDIR /app/dwengo
|
||||
|
||||
COPY package*.json ./
|
||||
COPY backend/package.json ./backend/
|
||||
# Backend depends on common
|
||||
COPY common/package.json ./common/
|
||||
|
||||
RUN npm install --silent --only=production
|
||||
|
||||
# Copy static files
|
||||
|
||||
COPY ./backend/i18n ./i18n
|
||||
|
@ -37,15 +44,6 @@ COPY --from=build-stage /app/dwengo/common/dist ./common/dist
|
|||
COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist
|
||||
COPY --from=build-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json
|
||||
|
||||
COPY package*.json ./
|
||||
COPY backend/package.json ./backend/
|
||||
# Backend depends on common
|
||||
COPY common/package.json ./common/
|
||||
|
||||
RUN npm install --silent --only=production
|
||||
|
||||
COPY ./backend/i18n ./backend/i18n
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"]
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.0.1",
|
||||
"express-fileupload": "^1.5.1",
|
||||
"express-jwt": "^8.5.1",
|
||||
"gift-pegjs": "^1.0.2",
|
||||
"isomorphic-dompurify": "^2.22.0",
|
||||
|
@ -37,8 +38,12 @@
|
|||
"jwks-rsa": "^3.1.0",
|
||||
"loki-logger-ts": "^1.0.2",
|
||||
"marked": "^15.0.7",
|
||||
"memjs": "^1.3.2",
|
||||
"mime-types": "^3.0.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"response-time": "^2.3.3",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"unzipper": "^0.12.3",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"winston-loki": "^6.1.3"
|
||||
|
@ -47,10 +52,14 @@
|
|||
"@mikro-orm/cli": "6.4.12",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/express-fileupload": "^1.5.1",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/memjs": "^1.3.3",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/response-time": "^2.3.8",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"globals": "^15.15.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.19.3",
|
||||
|
|
44
backend/src/caching.ts
Normal file
44
backend/src/caching.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { getLogger } from './logging/initalize.js';
|
||||
import { envVars, getEnvVar } from './util/envVars.js';
|
||||
import { Client } from 'memjs';
|
||||
|
||||
export type CacheClient = Client;
|
||||
|
||||
let cacheClient: CacheClient;
|
||||
|
||||
async function initializeClient(): Promise<CacheClient> {
|
||||
if (cacheClient !== undefined) {
|
||||
return cacheClient;
|
||||
}
|
||||
|
||||
const cachingHost = getEnvVar(envVars.CacheHost);
|
||||
const cachingPort = getEnvVar(envVars.CachePort);
|
||||
const cachingUrl = `${cachingHost}:${cachingPort}`;
|
||||
|
||||
if (cachingHost === '') {
|
||||
return cacheClient;
|
||||
}
|
||||
|
||||
cacheClient = Client.create(cachingUrl);
|
||||
|
||||
getLogger().info(`Memcached client initialized at ${cachingUrl}`);
|
||||
|
||||
return cacheClient;
|
||||
}
|
||||
|
||||
export async function getCacheClient(): Promise<CacheClient> {
|
||||
cacheClient ||= await initializeClient();
|
||||
return cacheClient;
|
||||
}
|
||||
|
||||
export async function checkCachingHealth(): Promise<boolean> {
|
||||
try {
|
||||
const client = await getCacheClient();
|
||||
await client.set('health', Buffer.from('ok'), { expires: 60 });
|
||||
const reply = await cacheClient.get('health');
|
||||
return reply?.value?.toString() === 'ok';
|
||||
} catch (error) {
|
||||
getLogger().error('Caching Health Check Failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import { UnauthorizedException } from '../exceptions/unauthorized-exception.js';
|
||||
import { getLogger } from '../logging/initalize.js';
|
||||
import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js';
|
||||
import { createOrUpdateStudent } from '../services/students.js';
|
||||
import { createOrUpdateTeacher } from '../services/teachers.js';
|
||||
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||
import { Response } from 'express';
|
||||
import { createOrUpdateStudent } from '../services/students.js';
|
||||
import { Request, Response } from 'express';
|
||||
import { createOrUpdateTeacher } from '../services/teachers.js';
|
||||
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||
|
||||
interface FrontendIdpConfig {
|
||||
authority: string;
|
||||
|
@ -40,6 +41,10 @@ export function getFrontendAuthConfig(): FrontendAuthConfig {
|
|||
};
|
||||
}
|
||||
|
||||
export function handleGetFrontendAuthConfig(_req: Request, res: Response): void {
|
||||
res.json(getFrontendAuthConfig());
|
||||
}
|
||||
|
||||
export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
const auth = req.auth;
|
||||
if (!auth) {
|
||||
|
@ -51,7 +56,7 @@ export async function postHelloHandler(req: AuthenticatedRequest, res: Response)
|
|||
firstName: auth.firstName ?? '',
|
||||
lastName: auth.lastName ?? '',
|
||||
};
|
||||
if (auth.accountType === 'student') {
|
||||
if (auth.accountType === AccountType.Student) {
|
||||
await createOrUpdateStudent(userData);
|
||||
logger.debug(`Synchronized student ${userData.username} with IDP`);
|
||||
} else {
|
||||
|
|
|
@ -7,6 +7,9 @@ import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
|||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||
import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
|
||||
import { UploadedFile } from 'express-fileupload';
|
||||
import { AuthenticatedRequest } from '../middleware/auth/authenticated-request';
|
||||
import { requireFields } from './error-helper.js';
|
||||
|
||||
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO {
|
||||
if (!req.params.hruid) {
|
||||
|
@ -20,27 +23,35 @@ function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIde
|
|||
}
|
||||
|
||||
function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentifier {
|
||||
if (!req.query.hruid) {
|
||||
throw new BadRequestException('HRUID is required.');
|
||||
}
|
||||
const { hruid, language } = req.params;
|
||||
requireFields({ hruid });
|
||||
|
||||
return {
|
||||
hruid: req.params.hruid,
|
||||
language: (req.query.language as Language) || FALLBACK_LANG,
|
||||
hruid,
|
||||
language: (language as Language) || FALLBACK_LANG,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAllLearningObjects(req: Request, res: Response): Promise<void> {
|
||||
const learningPathId = getLearningPathIdentifierFromRequest(req);
|
||||
const full = req.query.full;
|
||||
if (req.query.admin) {
|
||||
// If the admin query parameter is present, the user wants to have all learning objects with this admin.
|
||||
const learningObjects = await learningObjectService.getLearningObjectsAdministratedBy(req.query.admin as string);
|
||||
|
||||
let learningObjects: FilteredLearningObject[] | string[];
|
||||
if (full) {
|
||||
learningObjects = await learningObjectService.getLearningObjectsFromPath(learningPathId);
|
||||
res.json(learningObjects);
|
||||
} else {
|
||||
learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId);
|
||||
}
|
||||
// Else he/she wants all learning objects on the path specified by the request parameters.
|
||||
const learningPathId = getLearningPathIdentifierFromRequest(req);
|
||||
const full = req.query.full;
|
||||
|
||||
res.json({ learningObjects: learningObjects });
|
||||
let learningObjects: FilteredLearningObject[] | string[];
|
||||
if (full) {
|
||||
learningObjects = await learningObjectService.getLearningObjectsFromPath(learningPathId);
|
||||
} else {
|
||||
learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId);
|
||||
}
|
||||
|
||||
res.json({ learningObjects: learningObjects });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLearningObject(req: Request, res: Response): Promise<void> {
|
||||
|
@ -72,3 +83,32 @@ export async function getAttachment(req: Request, res: Response): Promise<void>
|
|||
}
|
||||
res.setHeader('Content-Type', attachment.mimeType).send(attachment.content);
|
||||
}
|
||||
|
||||
export async function handlePostLearningObject(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
if (!req.files || !req.files.learningObject) {
|
||||
throw new BadRequestException('No file uploaded');
|
||||
}
|
||||
const learningObject = await learningObjectService.storeLearningObject((req.files.learningObject as UploadedFile).tempFilePath, [
|
||||
req.auth!.username,
|
||||
]);
|
||||
res.json(learningObject);
|
||||
}
|
||||
|
||||
export async function handleDeleteLearningObject(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
const learningObjectId = getLearningObjectIdentifierFromRequest(req);
|
||||
|
||||
if (!learningObjectId.version) {
|
||||
throw new BadRequestException('When deleting a learning object, a version must be specified.');
|
||||
}
|
||||
|
||||
const deletedLearningObject = await learningObjectService.deleteLearningObject({
|
||||
hruid: learningObjectId.hruid,
|
||||
version: learningObjectId.version,
|
||||
language: learningObjectId.language,
|
||||
});
|
||||
if (deletedLearningObject) {
|
||||
res.json(deletedLearningObject);
|
||||
} else {
|
||||
throw new NotFoundException('Learning object not found');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,66 +7,116 @@ import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
|||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||
import { Group } from '../entities/assignments/group.entity.js';
|
||||
import { getAssignmentRepository, getGroupRepository } from '../data/repositories.js';
|
||||
import { AuthenticatedRequest } from '../middleware/auth/authenticated-request';
|
||||
import { LearningPath } from '@dwengo-1/common/interfaces/learning-content';
|
||||
import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js';
|
||||
import { LearningPath, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
|
||||
import { getTeacher } from '../services/teachers.js';
|
||||
import { requireFields } from './error-helper.js';
|
||||
|
||||
/**
|
||||
* Fetch learning paths based on query parameters.
|
||||
*/
|
||||
export async function getLearningPaths(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
const hruids = req.query.hruid;
|
||||
const themeKey = req.query.theme as string;
|
||||
const searchQuery = req.query.search as string;
|
||||
const language = (req.query.language as string) || FALLBACK_LANG;
|
||||
|
||||
const forGroupNo = req.query.forGroup as string;
|
||||
const assignmentNo = req.query.assignmentNo as string;
|
||||
const classId = req.query.classId as string;
|
||||
|
||||
let forGroup: Group | undefined;
|
||||
|
||||
if (forGroupNo) {
|
||||
if (!assignmentNo || !classId) {
|
||||
throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.');
|
||||
}
|
||||
const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo));
|
||||
if (assignment) {
|
||||
forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let hruidList;
|
||||
|
||||
if (hruids) {
|
||||
hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)];
|
||||
} else if (themeKey) {
|
||||
const theme = themes.find((t) => t.title === themeKey);
|
||||
if (theme) {
|
||||
hruidList = theme.hruids;
|
||||
} else {
|
||||
throw new NotFoundException(`Theme "${themeKey}" not found.`);
|
||||
}
|
||||
} else if (searchQuery) {
|
||||
const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup);
|
||||
res.json(searchResults);
|
||||
return;
|
||||
const admin = req.query.admin;
|
||||
if (admin) {
|
||||
const paths = await learningPathService.getLearningPathsAdministratedBy(admin as string);
|
||||
res.json(paths);
|
||||
} else {
|
||||
hruidList = themes.flatMap((theme) => theme.hruids);
|
||||
const hruids = req.query.hruid;
|
||||
const themeKey = req.query.theme as string;
|
||||
const searchQuery = req.query.search as string;
|
||||
const language = (req.query.language as string) || FALLBACK_LANG;
|
||||
|
||||
const apiLearningPathResponse = await learningPathService.fetchLearningPaths(hruidList, language as Language, 'All themes', forGroup);
|
||||
const apiLearningPaths: LearningPath[] = apiLearningPathResponse.data || [];
|
||||
let allLearningPaths: LearningPath[] = apiLearningPaths;
|
||||
const forGroupNo = req.query.forGroup as string;
|
||||
const assignmentNo = req.query.assignmentNo as string;
|
||||
const classId = req.query.classId as string;
|
||||
|
||||
if (req.auth) {
|
||||
const adminUsername = req.auth.username;
|
||||
const userLearningPaths = await learningPathService.searchLearningPathsByAdmin([adminUsername], language as Language, forGroup) || [];
|
||||
allLearningPaths = apiLearningPaths.concat(userLearningPaths);
|
||||
let forGroup: Group | undefined;
|
||||
|
||||
if (forGroupNo) {
|
||||
if (!assignmentNo || !classId) {
|
||||
throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.');
|
||||
}
|
||||
const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo));
|
||||
if (assignment) {
|
||||
forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
res.json(allLearningPaths);
|
||||
return;
|
||||
}
|
||||
let hruidList;
|
||||
|
||||
const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup);
|
||||
res.json(learningPaths.data);
|
||||
if (hruids) {
|
||||
hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)];
|
||||
} else if (themeKey) {
|
||||
const theme = themes.find((t) => t.title === themeKey);
|
||||
if (theme) {
|
||||
hruidList = theme.hruids;
|
||||
} else {
|
||||
throw new NotFoundException(`Theme "${themeKey}" not found.`);
|
||||
}
|
||||
} else if (searchQuery) {
|
||||
const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup);
|
||||
res.json(searchResults);
|
||||
return;
|
||||
} else {
|
||||
hruidList = themes.flatMap((theme) => theme.hruids);
|
||||
|
||||
const apiLearningPathResponse = await learningPathService.fetchLearningPaths(hruidList, language as Language, 'All themes', forGroup);
|
||||
const apiLearningPaths: LearningPath[] = apiLearningPathResponse.data || [];
|
||||
let allLearningPaths: LearningPath[] = apiLearningPaths;
|
||||
|
||||
if (req.auth) {
|
||||
const adminUsername = req.auth.username;
|
||||
const userLearningPaths = await learningPathService.searchLearningPathsByAdmin([adminUsername], language as Language, forGroup) || [];
|
||||
allLearningPaths = apiLearningPaths.concat(userLearningPaths);
|
||||
}
|
||||
|
||||
res.json(allLearningPaths);
|
||||
return;
|
||||
}
|
||||
|
||||
const learningPaths = await learningPathService.fetchLearningPaths(
|
||||
hruidList,
|
||||
language as Language,
|
||||
`HRUIDs: ${hruidList.join(', ')}`,
|
||||
forGroup,
|
||||
);
|
||||
res.json(learningPaths.data);
|
||||
}
|
||||
}
|
||||
|
||||
function postOrPutLearningPath(isPut: boolean): (req: AuthenticatedRequest, res: Response) => Promise<void> {
|
||||
return async (req, res) => {
|
||||
const path = req.body as LearningPath;
|
||||
const { hruid: hruidParam, language: languageParam } = req.params;
|
||||
|
||||
if (isPut) {
|
||||
requireFields({ hruidParam, languageParam, path });
|
||||
}
|
||||
|
||||
const teacher = await getTeacher(req.auth!.username);
|
||||
if (isPut) {
|
||||
if (req.params.hruid !== path.hruid || req.params.language !== path.language) {
|
||||
throw new BadRequestException('id_not_matching_query_params');
|
||||
}
|
||||
await learningPathService.deleteLearningPath({ hruid: path.hruid, language: path.language as Language });
|
||||
}
|
||||
res.json(await learningPathService.createNewLearningPath(path, [teacher]));
|
||||
};
|
||||
}
|
||||
|
||||
export const postLearningPath = postOrPutLearningPath(false);
|
||||
export const putLearningPath = postOrPutLearningPath(true);
|
||||
|
||||
export async function deleteLearningPath(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
const { hruid, language } = req.params;
|
||||
|
||||
requireFields({ hruid, language });
|
||||
|
||||
const id: LearningPathIdentifier = { hruid, language: language as Language };
|
||||
const deletedPath = await learningPathService.deleteLearningPath(id);
|
||||
if (deletedPath) {
|
||||
res.json(deletedPath);
|
||||
} else {
|
||||
throw new NotFoundException('The learning path could not be found.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ export async function createStudentRequestHandler(req: Request, res: Response):
|
|||
const classId = req.body.classId;
|
||||
requireFields({ username, classId });
|
||||
|
||||
const request = await createClassJoinRequest(username, classId);
|
||||
const request = await createClassJoinRequest(username, classId.toUpperCase());
|
||||
res.json({ request });
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,11 @@ export async function getAllSubmissionsHandler(req: Request, res: Response): Pro
|
|||
|
||||
// TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
|
||||
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||
const submitter = req.body.submitter;
|
||||
const usernameSubmitter = req.body.submitter.username;
|
||||
const group = req.body.group;
|
||||
requireFields({ group, submitter, usernameSubmitter });
|
||||
|
||||
const submissionDTO = req.body as SubmissionDTO;
|
||||
const submission = await createSubmission(submissionDTO);
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Request, Response } from 'express';
|
|||
import { requireFields } from './error-helper.js';
|
||||
import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js';
|
||||
import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation';
|
||||
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||
|
||||
export async function getAllInvitationsHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
|
@ -30,6 +31,10 @@ export async function createInvitationHandler(req: Request, res: Response): Prom
|
|||
const classId = req.body.class;
|
||||
requireFields({ sender, receiver, classId });
|
||||
|
||||
if (sender === receiver) {
|
||||
throw new ConflictException('Cannot send an invitation to yourself');
|
||||
}
|
||||
|
||||
const data = req.body as TeacherInvitationData;
|
||||
const invitation = await createInvitation(data);
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
getJoinRequestsByClass,
|
||||
getStudentsByTeacher,
|
||||
getTeacher,
|
||||
getTeacherQuestions,
|
||||
updateClassJoinRequestStatus,
|
||||
} from '../services/teachers.js';
|
||||
import { requireFields } from './error-helper.js';
|
||||
|
@ -70,16 +69,6 @@ export async function getTeacherStudentHandler(req: Request, res: Response): Pro
|
|||
res.json({ students });
|
||||
}
|
||||
|
||||
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
const full = req.query.full === 'true';
|
||||
requireFields({ username });
|
||||
|
||||
const questions = await getTeacherQuestions(username, full);
|
||||
|
||||
res.json({ questions });
|
||||
}
|
||||
|
||||
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.classId;
|
||||
requireFields({ classId });
|
||||
|
|
|
@ -2,7 +2,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
|||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||
|
||||
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
||||
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||
|
@ -13,7 +12,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
|||
version: identifier.version,
|
||||
},
|
||||
{
|
||||
populate: ['keywords'],
|
||||
populate: ['keywords', 'admins'],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -33,10 +32,22 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
|||
);
|
||||
}
|
||||
|
||||
public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
||||
public async findAllByAdmin(adminUsername: string): Promise<LearningObject[]> {
|
||||
return this.find(
|
||||
{ admins: teacher },
|
||||
{
|
||||
admins: {
|
||||
username: adminUsername,
|
||||
},
|
||||
},
|
||||
{ populate: ['admins'] } // Make sure to load admin relations
|
||||
);
|
||||
}
|
||||
|
||||
public async removeByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||
const learningObject = await this.findByIdentifier(identifier);
|
||||
if (learningObject) {
|
||||
await this.em.removeAndFlush(learningObject);
|
||||
}
|
||||
return learningObject;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ import { MatchMode } from '@dwengo-1/common/util/match-mode';
|
|||
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
|
||||
import { RequiredEntityData } from '@mikro-orm/core';
|
||||
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||
import { EntityAlreadyExistsException } from '../../exceptions/entity-already-exists-exception.js';
|
||||
import { Teacher } from '../../entities/users/teacher.entity';
|
||||
|
||||
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
|
||||
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', 'admins'] });
|
||||
}
|
||||
|
||||
public async findByAdmins(admins: Teacher[], language: Language, _matchMode?: MatchMode): Promise<LearningPath[]> {
|
||||
|
@ -38,7 +40,21 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
|
|||
language: language,
|
||||
$or: [{ title: { $like: `%${query}%` } }, { description: { $like: `%${query}%` } }],
|
||||
},
|
||||
populate: ['nodes', 'nodes.transitions'],
|
||||
populate: ['nodes', 'nodes.transitions', 'admins'],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all learning paths which have the user with the given username as an administrator.
|
||||
*/
|
||||
public async findAllByAdminUsername(adminUsername: string): Promise<LearningPath[]> {
|
||||
return this.findAll({
|
||||
where: {
|
||||
admins: {
|
||||
username: adminUsername,
|
||||
},
|
||||
},
|
||||
populate: ['nodes', 'nodes.transitions', 'admins'],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -50,18 +66,15 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
|
|||
return this.em.create(LearningPathTransition, transitionData);
|
||||
}
|
||||
|
||||
public async saveLearningPathNodesAndTransitions(
|
||||
path: LearningPath,
|
||||
nodes: LearningPathNode[],
|
||||
transitions: LearningPathTransition[],
|
||||
options?: { preventOverwrite?: boolean }
|
||||
): Promise<void> {
|
||||
if (options?.preventOverwrite && (await this.findOne(path))) {
|
||||
throw new EntityAlreadyExistsException('A learning path with this hruid/language combination already exists.');
|
||||
/**
|
||||
* Deletes the learning path with the given hruid and language.
|
||||
* @returns the deleted learning path or null if it was not found.
|
||||
*/
|
||||
public async deleteByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
|
||||
const path = await this.findByHruidAndLanguage(hruid, language);
|
||||
if (path) {
|
||||
await this.em.removeAndFlush(path);
|
||||
}
|
||||
const em = this.getEntityManager();
|
||||
await em.persistAndFlush(path);
|
||||
await Promise.all(nodes.map(async (it) => em.persistAndFlush(it)));
|
||||
await Promise.all(transitions.map(async (it) => em.persistAndFlush(it)));
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ import { Question } from '../../entities/questions/question.entity.js';
|
|||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||
import { Student } from '../../entities/users/student.entity.js';
|
||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||
import { Group } from '../../entities/assignments/group.entity.js';
|
||||
import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
||||
import { Loaded } from '@mikro-orm/core';
|
||||
import { Group } from '../../entities/assignments/group.entity';
|
||||
|
||||
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||
public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> {
|
||||
|
|
|
@ -26,6 +26,9 @@ export class Assignment {
|
|||
@Property({ type: 'string' })
|
||||
learningPathHruid!: string;
|
||||
|
||||
@Property({ type: 'datetime', nullable: true })
|
||||
deadline?: Date;
|
||||
|
||||
@Enum({
|
||||
items: () => Language,
|
||||
})
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { v4 } from 'uuid';
|
||||
import { Teacher } from '../users/teacher.entity.js';
|
||||
import { Student } from '../users/student.entity.js';
|
||||
import { ClassRepository } from '../../data/classes/class-repository.js';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
const generateClassId = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 6);
|
||||
|
||||
@Entity({
|
||||
repository: () => ClassRepository,
|
||||
})
|
||||
export class Class {
|
||||
@PrimaryKey()
|
||||
classId? = v4();
|
||||
classId? = generateClassId();
|
||||
|
||||
@Property({ type: 'string' })
|
||||
displayName!: string;
|
||||
|
|
|
@ -9,6 +9,7 @@ export class Attachment {
|
|||
@ManyToOne({
|
||||
entity: () => LearningObject,
|
||||
primary: true,
|
||||
deleteRule: 'cascade',
|
||||
})
|
||||
learningObject!: LearningObject;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ArrayType, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { ArrayType, Collection, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Attachment } from './attachment.entity.js';
|
||||
import { Teacher } from '../users/teacher.entity.js';
|
||||
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
|
||||
|
@ -28,7 +28,7 @@ export class LearningObject {
|
|||
@ManyToMany({
|
||||
entity: () => Teacher,
|
||||
})
|
||||
admins!: Teacher[];
|
||||
admins: Collection<Teacher> = new Collection<Teacher>(this);
|
||||
|
||||
@Property({ type: 'string' })
|
||||
title!: string;
|
||||
|
@ -84,7 +84,7 @@ export class LearningObject {
|
|||
entity: () => Attachment,
|
||||
mappedBy: 'learningObject',
|
||||
})
|
||||
attachments: Attachment[] = [];
|
||||
attachments: Collection<Attachment> = new Collection<Attachment>(this);
|
||||
|
||||
@Property({ type: 'blob' })
|
||||
content!: Buffer;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core';
|
||||
import { Cascade, Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core';
|
||||
import { LearningPath } from './learning-path.entity.js';
|
||||
import { LearningPathTransition } from './learning-path-transition.entity.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
@ -26,7 +26,7 @@ export class LearningPathNode {
|
|||
@Property({ type: 'bool' })
|
||||
startNode!: boolean;
|
||||
|
||||
@OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' })
|
||||
@OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node', cascade: [Cascade.ALL] })
|
||||
transitions!: Collection<LearningPathTransition>;
|
||||
|
||||
@Property({ length: 3 })
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Cascade, Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Teacher } from '../users/teacher.entity.js';
|
||||
import { LearningPathRepository } from '../../data/content/learning-path-repository.js';
|
||||
import { LearningPathNode } from './learning-path-node.entity.js';
|
||||
|
@ -24,6 +24,6 @@ export class LearningPath {
|
|||
@Property({ type: 'blob', nullable: true })
|
||||
image: Buffer | null = null;
|
||||
|
||||
@OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' })
|
||||
@OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath', cascade: [Cascade.ALL] })
|
||||
nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
|
|||
description: assignment.description,
|
||||
learningPath: assignment.learningPathHruid,
|
||||
language: assignment.learningPathLanguage,
|
||||
deadline: assignment.deadline ?? new Date(),
|
||||
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
|
||||
};
|
||||
}
|
||||
|
@ -31,6 +32,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
|
|||
description: assignmentData.description,
|
||||
learningPathHruid: assignmentData.learningPath,
|
||||
learningPathLanguage: languageMap[assignmentData.language],
|
||||
deadline: assignmentData.deadline,
|
||||
groups: [],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@ export function mapToUserDTO(user: User): UserDTO {
|
|||
};
|
||||
}
|
||||
|
||||
export function mapToUsername(user: { username: string }): string {
|
||||
return user.username;
|
||||
}
|
||||
|
||||
export function mapToUser<T extends User>(userData: UserDTO, userInstance: T): T {
|
||||
userInstance.username = userData.username;
|
||||
userInstance.firstName = userData.firstName;
|
||||
|
|
|
@ -29,7 +29,8 @@ function initializeLogger(): Logger {
|
|||
format: format.combine(format.cli(), format.simple()),
|
||||
});
|
||||
|
||||
if (getEnvVar(envVars.RunMode) === 'dev') {
|
||||
const runMode = getEnvVar(envVars.RunMode);
|
||||
if (runMode === 'dev' || runMode.includes('test')) {
|
||||
logger = createLogger({
|
||||
transports: [consoleTransport],
|
||||
});
|
||||
|
|
|
@ -7,7 +7,6 @@ import * as express from 'express';
|
|||
import { AuthenticatedRequest } from './authenticated-request.js';
|
||||
import { AuthenticationInfo } from './authentication-info.js';
|
||||
import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js';
|
||||
import { ForbiddenException } from '../../exceptions/forbidden-exception.js';
|
||||
|
||||
const JWKS_CACHE = true;
|
||||
const JWKS_RATE_LIMIT = true;
|
||||
|
@ -108,36 +107,3 @@ function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response
|
|||
}
|
||||
|
||||
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
||||
|
||||
/**
|
||||
* Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill
|
||||
* the given access condition.
|
||||
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
|
||||
* to true.
|
||||
*/
|
||||
export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) {
|
||||
return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
|
||||
if (!req.auth) {
|
||||
throw new UnauthorizedException();
|
||||
} else if (!accessCondition(req.auth)) {
|
||||
throw new ForbiddenException();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware which rejects all unauthenticated users, but accepts all authenticated users.
|
||||
*/
|
||||
export const authenticatedOnly = authorize((_) => true);
|
||||
|
||||
/**
|
||||
* Middleware which rejects requests from unauthenticated users or users that aren't students.
|
||||
*/
|
||||
export const studentsOnly = authorize((auth) => auth.accountType === 'student');
|
||||
|
||||
/**
|
||||
* Middleware which rejects requests from unauthenticated users or users that aren't teachers.
|
||||
*/
|
||||
export const teachersOnly = authorize((auth) => auth.accountType === 'teacher');
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import { Request } from 'express';
|
||||
import { JwtPayload } from 'jsonwebtoken';
|
||||
import { AuthenticationInfo } from './authentication-info.js';
|
||||
import * as core from 'express-serve-static-core';
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
export interface AuthenticatedRequest<
|
||||
P = core.ParamsDictionary,
|
||||
ResBody = unknown,
|
||||
ReqBody = unknown,
|
||||
ReqQuery = core.Query,
|
||||
Locals extends Record<string, unknown> = Record<string, unknown>,
|
||||
> extends Request<P, ResBody, ReqBody, ReqQuery, Locals> {
|
||||
// Properties are optional since the user is not necessarily authenticated.
|
||||
jwtPayload?: JwtPayload;
|
||||
auth?: AuthenticationInfo;
|
||||
|
|
21
backend/src/middleware/auth/checks/assignment-auth-checks.ts
Normal file
21
backend/src/middleware/auth/checks/assignment-auth-checks.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { authorize } from './auth-checks.js';
|
||||
import { fetchClass } from '../../../services/classes.js';
|
||||
import { fetchAllGroups } from '../../../services/groups.js';
|
||||
import { mapToUsername } from '../../../interfaces/user.js';
|
||||
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||
|
||||
/**
|
||||
* Expects the path to contain the path parameters 'classId' and 'id' (meaning the ID of the assignment).
|
||||
* Only allows requests from users who are
|
||||
* - either teachers of the class the assignment was posted in,
|
||||
* - or students in a group of the assignment.
|
||||
*/
|
||||
export const onlyAllowIfHasAccessToAssignment = authorize(async (auth, req) => {
|
||||
const { classid: classId, id: assignmentId } = req.params as { classid: string; id: number };
|
||||
if (auth.accountType === AccountType.Teacher) {
|
||||
const clazz = await fetchClass(classId);
|
||||
return clazz.teachers.map(mapToUsername).includes(auth.username);
|
||||
}
|
||||
const groups = await fetchAllGroups(classId, assignmentId);
|
||||
return groups.some((group) => group.members.map((member) => member.username).includes(auth.username));
|
||||
});
|
61
backend/src/middleware/auth/checks/auth-checks.ts
Normal file
61
backend/src/middleware/auth/checks/auth-checks.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { AuthenticationInfo } from '../authentication-info.js';
|
||||
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||
import * as express from 'express';
|
||||
import { RequestHandler } from 'express';
|
||||
import { UnauthorizedException } from '../../../exceptions/unauthorized-exception.js';
|
||||
import { ForbiddenException } from '../../../exceptions/forbidden-exception.js';
|
||||
import { envVars, getEnvVar } from '../../../util/envVars.js';
|
||||
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||
|
||||
/**
|
||||
* Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill
|
||||
* the given access condition.
|
||||
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
|
||||
* to true.
|
||||
*/
|
||||
export function authorize<P, ResBody, ReqBody, ReqQuery, Locals extends Record<string, unknown>>(
|
||||
accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>) => boolean | Promise<boolean>
|
||||
): RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals> {
|
||||
// Bypass authentication during testing
|
||||
if (getEnvVar(envVars.RunMode) === 'test') {
|
||||
return async (
|
||||
_req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>,
|
||||
_res: express.Response,
|
||||
next: express.NextFunction
|
||||
): Promise<void> => {
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
return async (
|
||||
req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>,
|
||||
_res: express.Response,
|
||||
next: express.NextFunction
|
||||
): Promise<void> => {
|
||||
if (!req.auth) {
|
||||
throw new UnauthorizedException();
|
||||
} else if (!(await accessCondition(req.auth, req))) {
|
||||
throw new ForbiddenException();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware which rejects all unauthenticated users, but accepts all authenticated users.
|
||||
*/
|
||||
export const authenticatedOnly = authorize((_) => true);
|
||||
/**
|
||||
* Middleware which rejects requests from unauthenticated users or users that aren't students.
|
||||
*/
|
||||
export const studentsOnly = authorize((auth) => auth.accountType === AccountType.Student);
|
||||
/**
|
||||
* Middleware which rejects requests from unauthenticated users or users that aren't teachers.
|
||||
*/
|
||||
export const teachersOnly = authorize((auth) => auth.accountType === AccountType.Teacher);
|
||||
/**
|
||||
* Middleware which is to be used on requests no normal user should be able to execute.
|
||||
* Since there is no concept of administrator accounts yet, currently, those requests will always be blocked.
|
||||
*/
|
||||
export const adminOnly = authorize(() => false);
|
70
backend/src/middleware/auth/checks/class-auth-checks.ts
Normal file
70
backend/src/middleware/auth/checks/class-auth-checks.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { authorize } from './auth-checks.js';
|
||||
import { AuthenticationInfo } from '../authentication-info.js';
|
||||
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||
import { fetchClass } from '../../../services/classes.js';
|
||||
import { mapToUsername } from '../../../interfaces/user.js';
|
||||
import { getAllInvitations } from '../../../services/teacher-invitations.js';
|
||||
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||
|
||||
async function teaches(teacherUsername: string, classId: string): Promise<boolean> {
|
||||
const clazz = await fetchClass(classId);
|
||||
return clazz.teachers.map(mapToUsername).includes(teacherUsername);
|
||||
}
|
||||
|
||||
/**
|
||||
* To be used on a request with path parameters username and classId.
|
||||
* Only allows requests whose username parameter is equal to the username of the user who is logged in and requests
|
||||
* whose classId parameter references a class the logged-in user is a teacher of.
|
||||
*/
|
||||
export const onlyAllowStudentHimselfAndTeachersOfClass = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
if (req.params.username === auth.username) {
|
||||
return true;
|
||||
} else if (auth.accountType === AccountType.Teacher) {
|
||||
return teaches(auth.username, req.params.classId);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
/**
|
||||
* Only let the request pass through if its path parameter "username" is the username of the currently logged-in
|
||||
* teacher and the path parameter "classId" refers to a class the teacher teaches.
|
||||
*/
|
||||
export const onlyAllowTeacherOfClass = authorize(
|
||||
async (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username && teaches(auth.username, req.params.classId)
|
||||
);
|
||||
|
||||
/**
|
||||
* Only let the request pass through if the class id in it refers to a class the current user is in (as a student
|
||||
* or teacher)
|
||||
*/
|
||||
export const onlyAllowIfInClass = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const classId = req.params.classId ?? req.params.classid ?? req.params.id;
|
||||
const clazz = await fetchClass(classId);
|
||||
if (auth.accountType === AccountType.Teacher) {
|
||||
return clazz.teachers.map(mapToUsername).includes(auth.username);
|
||||
}
|
||||
return clazz.students.map(mapToUsername).includes(auth.username);
|
||||
});
|
||||
|
||||
export const onlyAllowIfInClassOrInvited = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const classId = req.params.classId ?? req.params.classid ?? req.params.id;
|
||||
const clazz = await fetchClass(classId);
|
||||
if (auth.accountType === AccountType.Teacher) {
|
||||
const invitations = await getAllInvitations(auth.username, false);
|
||||
return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some((invitation) => invitation.classId === classId);
|
||||
}
|
||||
return clazz.students.map(mapToUsername).includes(auth.username);
|
||||
});
|
||||
|
||||
/**
|
||||
* Only allows the request to pass if the 'class' property in its body is a class the current user is a member of.
|
||||
*/
|
||||
export const onlyAllowOwnClassInBody = authorize(async (auth, req) => {
|
||||
const classId = (req.body as { class: string })?.class;
|
||||
const clazz = await fetchClass(classId);
|
||||
|
||||
if (auth.accountType === AccountType.Teacher) {
|
||||
return clazz.teachers.map(mapToUsername).includes(auth.username);
|
||||
}
|
||||
return clazz.students.map(mapToUsername).includes(auth.username);
|
||||
});
|
26
backend/src/middleware/auth/checks/group-auth-checker.ts
Normal file
26
backend/src/middleware/auth/checks/group-auth-checker.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { authorize } from './auth-checks.js';
|
||||
import { fetchClass } from '../../../services/classes.js';
|
||||
import { fetchGroup } from '../../../services/groups.js';
|
||||
import { mapToUsername } from '../../../interfaces/user.js';
|
||||
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||
|
||||
/**
|
||||
* Expects the path to contain the path parameters 'classid', 'assignmentid' and 'groupid'.
|
||||
* Only allows requests from users who are
|
||||
* - either teachers of the class the assignment for the group was posted in,
|
||||
* - or students in the group
|
||||
*/
|
||||
export const onlyAllowIfHasAccessToGroup = authorize(async (auth, req) => {
|
||||
const {
|
||||
classid: classId,
|
||||
assignmentid: assignmentId,
|
||||
groupid: groupId,
|
||||
} = req.params as { classid: string; assignmentid: number; groupid: number };
|
||||
|
||||
if (auth.accountType === AccountType.Teacher) {
|
||||
const clazz = await fetchClass(classId);
|
||||
return clazz.teachers.map(mapToUsername).includes(auth.username);
|
||||
} // User is student
|
||||
const group = await fetchGroup(classId, assignmentId, groupId);
|
||||
return group.members.map(mapToUsername).includes(auth.username);
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
import { authorize } from './auth-checks';
|
||||
import { AuthenticationInfo } from '../authentication-info';
|
||||
import { AuthenticatedRequest } from '../authenticated-request';
|
||||
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||
|
||||
/**
|
||||
* Only allows requests whose learning path personalization query parameters ('forGroup' / 'assignmentNo' / 'classId')
|
||||
* are
|
||||
* - either not set
|
||||
* - or set to a group the user is in,
|
||||
* - or set to anything if the user is a teacher.
|
||||
*/
|
||||
export const onlyAllowPersonalizationForOwnGroup = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const { forGroup, assignmentNo, classId } = req.params;
|
||||
if (auth.accountType === AccountType.Student && forGroup && assignmentNo && classId) {
|
||||
// TODO: groupNumber?
|
||||
// Const group = await fetchGroup(Number(classId), Number(assignmentNo), )
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import learningObjectService from '../../../services/learning-objects/learning-object-service.js';
|
||||
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||
import { AuthenticationInfo } from '../authentication-info.js';
|
||||
import { authorize } from './auth-checks.js';
|
||||
|
||||
export const onlyAdminsForLearningObject = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const { hruid } = req.params;
|
||||
const { version, language } = req.query;
|
||||
const admins = await learningObjectService.getAdmins({
|
||||
hruid,
|
||||
language: language as Language,
|
||||
version: parseInt(version as string),
|
||||
});
|
||||
return admins.includes(auth.username);
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import learningPathService from '../../../services/learning-paths/learning-path-service.js';
|
||||
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||
import { AuthenticationInfo } from '../authentication-info.js';
|
||||
import { authorize } from './auth-checks.js';
|
||||
|
||||
export const onlyAdminsForLearningPath = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const adminsForLearningPath = await learningPathService.getAdmins({
|
||||
hruid: req.params.hruid,
|
||||
language: req.params.language as Language,
|
||||
});
|
||||
return adminsForLearningPath && adminsForLearningPath.includes(auth.username);
|
||||
});
|
66
backend/src/middleware/auth/checks/question-checks.ts
Normal file
66
backend/src/middleware/auth/checks/question-checks.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { authorize } from './auth-checks.js';
|
||||
import { AuthenticationInfo } from '../authentication-info.js';
|
||||
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||
import { requireFields } from '../../../controllers/error-helper.js';
|
||||
import { getLearningObjectId, getQuestionId } from '../../../controllers/questions.js';
|
||||
import { fetchQuestion } from '../../../services/questions.js';
|
||||
import { FALLBACK_SEQ_NUM } from '../../../config.js';
|
||||
import { fetchAnswer } from '../../../services/answers.js';
|
||||
import { mapToUsername } from '../../../interfaces/user.js';
|
||||
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||
|
||||
export const onlyAllowAuthor = authorize(
|
||||
(auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { author: string }).author === auth.username
|
||||
);
|
||||
|
||||
export const onlyAllowAuthorRequest = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const hruid = req.params.hruid;
|
||||
const version = req.params.version;
|
||||
const language = req.query.lang as string;
|
||||
const seq = req.params.seq;
|
||||
requireFields({ hruid });
|
||||
|
||||
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||
const questionId = getQuestionId(learningObjectId, seq);
|
||||
|
||||
const question = await fetchQuestion(questionId);
|
||||
|
||||
return question.author.username === auth.username;
|
||||
});
|
||||
|
||||
export const onlyAllowAuthorRequestAnswer = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const hruid = req.params.hruid;
|
||||
const version = req.params.version;
|
||||
const language = req.query.lang as string;
|
||||
const seq = req.params.seq;
|
||||
const seqAnswer = req.params.seqAnswer;
|
||||
requireFields({ hruid });
|
||||
|
||||
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||
const questionId = getQuestionId(learningObjectId, seq);
|
||||
|
||||
const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM;
|
||||
const answer = await fetchAnswer(questionId, sequenceNumber);
|
||||
|
||||
return answer.author.username === auth.username;
|
||||
});
|
||||
|
||||
export const onlyAllowIfHasAccessToQuestion = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const hruid = req.params.hruid;
|
||||
const version = req.params.version;
|
||||
const language = req.query.lang as string;
|
||||
const seq = req.params.seq;
|
||||
requireFields({ hruid });
|
||||
|
||||
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||
const questionId = getQuestionId(learningObjectId, seq);
|
||||
|
||||
const question = await fetchQuestion(questionId);
|
||||
const group = question.inGroup;
|
||||
|
||||
if (auth.accountType === AccountType.Teacher) {
|
||||
const cls = group.assignment.within; // TODO check if contains full objects
|
||||
return cls.teachers.map(mapToUsername).includes(auth.username);
|
||||
} // User is student
|
||||
return group.members.map(mapToUsername).includes(auth.username);
|
||||
});
|
28
backend/src/middleware/auth/checks/submission-checks.ts
Normal file
28
backend/src/middleware/auth/checks/submission-checks.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { languageMap } from '@dwengo-1/common/util/language';
|
||||
import { LearningObjectIdentifier } from '../../../entities/content/learning-object-identifier.js';
|
||||
import { fetchSubmission } from '../../../services/submissions.js';
|
||||
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||
import { AuthenticationInfo } from '../authentication-info.js';
|
||||
import { authorize } from './auth-checks.js';
|
||||
import { FALLBACK_LANG } from '../../../config.js';
|
||||
import { mapToUsername } from '../../../interfaces/user.js';
|
||||
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||
|
||||
export const onlyAllowSubmitter = authorize(
|
||||
(auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username
|
||||
);
|
||||
|
||||
export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const { hruid: lohruid, id: submissionNumber } = req.params;
|
||||
const { language: lang, version: version } = req.query;
|
||||
|
||||
const loId = new LearningObjectIdentifier(lohruid, languageMap[lang as string] ?? FALLBACK_LANG, Number(version));
|
||||
const submission = await fetchSubmission(loId, Number(submissionNumber));
|
||||
|
||||
if (auth.accountType === AccountType.Teacher) {
|
||||
// Dit kan niet werken om dat al deze objecten niet gepopulate zijn.
|
||||
return submission.onBehalfOf.assignment.within.teachers.map(mapToUsername).includes(auth.username);
|
||||
}
|
||||
|
||||
return submission.onBehalfOf.members.map(mapToUsername).includes(auth.username);
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { authorize } from './auth-checks.js';
|
||||
import { AuthenticationInfo } from '../authentication-info.js';
|
||||
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||
|
||||
export const onlyAllowSenderOrReceiver = authorize(
|
||||
(auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.sender === auth.username || req.params.receiver === auth.username
|
||||
);
|
||||
|
||||
export const onlyAllowSender = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.sender === auth.username);
|
||||
|
||||
export const onlyAllowSenderBody = authorize(
|
||||
(auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { sender: string }).sender === auth.username
|
||||
);
|
||||
|
||||
export const onlyAllowReceiverBody = authorize(
|
||||
(auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { receiver: string }).receiver === auth.username
|
||||
);
|
8
backend/src/middleware/auth/checks/user-auth-checks.ts
Normal file
8
backend/src/middleware/auth/checks/user-auth-checks.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { authorize } from './auth-checks.js';
|
||||
import { AuthenticationInfo } from '../authentication-info.js';
|
||||
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||
|
||||
/**
|
||||
* Only allow the user whose username is in the path parameter "username" to access the endpoint.
|
||||
*/
|
||||
export const preventImpersonation = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username);
|
|
@ -17,7 +17,7 @@ export async function initORM(testingMode = false): Promise<MikroORM<IDatabaseDr
|
|||
|
||||
// Update the database scheme if necessary and enabled.
|
||||
if (getEnvVar(envVars.DbUpdate)) {
|
||||
logger.info("MikroORM: Updating database schema");
|
||||
logger.info('MikroORM: Updating database schema');
|
||||
await orm.schema.updateSchema();
|
||||
} else {
|
||||
const diff = await orm.schema.getUpdateSchemaSQL();
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import express from 'express';
|
||||
import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js';
|
||||
import { authenticatedOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
import { onlyAllowAuthor, onlyAllowAuthorRequestAnswer, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get('/', getAllAnswersHandler);
|
||||
router.get('/', authenticatedOnly, getAllAnswersHandler);
|
||||
|
||||
router.post('/', createAnswerHandler);
|
||||
router.post('/', teachersOnly, onlyAllowAuthor, createAnswerHandler);
|
||||
|
||||
router.get('/:seqAnswer', getAnswerHandler);
|
||||
router.get('/:seqAnswer', onlyAllowIfHasAccessToQuestion, getAnswerHandler);
|
||||
|
||||
router.delete('/:seqAnswer', deleteAnswerHandler);
|
||||
router.delete('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, deleteAnswerHandler);
|
||||
|
||||
router.put('/:seqAnswer', updateAnswerHandler);
|
||||
router.put('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, updateAnswerHandler);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -9,22 +9,25 @@ import {
|
|||
putAssignmentHandler,
|
||||
} from '../controllers/assignments.js';
|
||||
import groupRouter from './groups.js';
|
||||
import { teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
import { onlyAllowIfInClass } from '../middleware/auth/checks/class-auth-checks.js';
|
||||
import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks.js';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get('/', getAllAssignmentsHandler);
|
||||
router.get('/', teachersOnly, onlyAllowIfInClass, getAllAssignmentsHandler);
|
||||
|
||||
router.post('/', createAssignmentHandler);
|
||||
router.post('/', teachersOnly, onlyAllowIfInClass, createAssignmentHandler);
|
||||
|
||||
router.get('/:id', getAssignmentHandler);
|
||||
router.get('/:id', onlyAllowIfHasAccessToAssignment, getAssignmentHandler);
|
||||
|
||||
router.put('/:id', putAssignmentHandler);
|
||||
router.put('/:id', teachersOnly, onlyAllowIfHasAccessToAssignment, putAssignmentHandler);
|
||||
|
||||
router.delete('/:id', deleteAssignmentHandler);
|
||||
router.delete('/:id', teachersOnly, onlyAllowIfHasAccessToAssignment, deleteAssignmentHandler);
|
||||
|
||||
router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
|
||||
router.get('/:id/submissions', teachersOnly, onlyAllowIfHasAccessToAssignment, getAssignmentsSubmissionsHandler);
|
||||
|
||||
router.get('/:id/questions', getAssignmentQuestionsHandler);
|
||||
router.get('/:id/questions', teachersOnly, onlyAllowIfHasAccessToAssignment, getAssignmentQuestionsHandler);
|
||||
|
||||
router.use('/:assignmentid/groups', groupRouter);
|
||||
|
||||
|
|
|
@ -1,28 +1,35 @@
|
|||
import express from 'express';
|
||||
import { getFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js';
|
||||
import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js';
|
||||
import { handleGetFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js';
|
||||
import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Returns auth configuration for frontend
|
||||
router.get('/config', (_req, res) => {
|
||||
res.json(getFrontendAuthConfig());
|
||||
});
|
||||
router.get('/config', handleGetFrontendAuthConfig);
|
||||
|
||||
router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => {
|
||||
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
|
||||
/* #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||
res.json({ message: 'If you see this, you should be authenticated!' });
|
||||
});
|
||||
|
||||
router.get('/testStudentsOnly', studentsOnly, (_req, res) => {
|
||||
/* #swagger.security = [{ "student": [ ] }] */
|
||||
/* #swagger.security = [{ "studentProduction": [ ] }, { "studentStaging": [ ] }, { "studentDev": [ ] }] */
|
||||
res.json({ message: 'If you see this, you should be a student!' });
|
||||
});
|
||||
|
||||
router.get('/testTeachersOnly', teachersOnly, (_req, res) => {
|
||||
/* #swagger.security = [{ "teacher": [ ] }] */
|
||||
/* #swagger.security = [{ "teacherProduction": [ ] }, { "teacherStaging": [ ] }, { "teacherDev": [ ] }] */
|
||||
res.json({ message: 'If you see this, you should be a teacher!' });
|
||||
});
|
||||
|
||||
router.post('/hello', authenticatedOnly, postHelloHandler);
|
||||
// This endpoint is called by the client when the user has just logged in.
|
||||
// It creates or updates the user entity based on the authentication data the endpoint was called with.
|
||||
router.post(
|
||||
'/hello',
|
||||
authenticatedOnly,
|
||||
/*
|
||||
#swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }]
|
||||
*/ postHelloHandler
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -14,33 +14,35 @@ import {
|
|||
putClassHandler,
|
||||
} from '../controllers/classes.js';
|
||||
import assignmentRouter from './assignments.js';
|
||||
import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
import { onlyAllowIfInClass, onlyAllowIfInClassOrInvited } from '../middleware/auth/checks/class-auth-checks.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Root endpoint used to search objects
|
||||
router.get('/', getAllClassesHandler);
|
||||
router.get('/', adminOnly, getAllClassesHandler);
|
||||
|
||||
router.post('/', createClassHandler);
|
||||
router.post('/', teachersOnly, createClassHandler);
|
||||
|
||||
router.get('/:id', getClassHandler);
|
||||
router.get('/:id', onlyAllowIfInClassOrInvited, getClassHandler);
|
||||
|
||||
router.put('/:id', putClassHandler);
|
||||
router.put('/:id', teachersOnly, onlyAllowIfInClass, putClassHandler);
|
||||
|
||||
router.delete('/:id', deleteClassHandler);
|
||||
router.delete('/:id', teachersOnly, onlyAllowIfInClass, deleteClassHandler);
|
||||
|
||||
router.get('/:id/teacher-invitations', getTeacherInvitationsHandler);
|
||||
router.get('/:id/teacher-invitations', teachersOnly, onlyAllowIfInClass, getTeacherInvitationsHandler);
|
||||
|
||||
router.get('/:id/students', getClassStudentsHandler);
|
||||
router.get('/:id/students', onlyAllowIfInClass, getClassStudentsHandler);
|
||||
|
||||
router.post('/:id/students', addClassStudentHandler);
|
||||
router.post('/:id/students', teachersOnly, onlyAllowIfInClass, addClassStudentHandler);
|
||||
|
||||
router.delete('/:id/students/:username', deleteClassStudentHandler);
|
||||
router.delete('/:id/students/:username', teachersOnly, onlyAllowIfInClass, deleteClassStudentHandler);
|
||||
|
||||
router.get('/:id/teachers', getClassTeachersHandler);
|
||||
router.get('/:id/teachers', onlyAllowIfInClass, getClassTeachersHandler);
|
||||
|
||||
router.post('/:id/teachers', addClassTeacherHandler);
|
||||
// De combinatie van deze POST en DELETE endpoints kan lethal zijn
|
||||
router.post('/:id/teachers', teachersOnly, onlyAllowIfInClass, addClassTeacherHandler);
|
||||
|
||||
router.delete('/:id/teachers/:username', deleteClassTeacherHandler);
|
||||
router.delete('/:id/teachers/:username', teachersOnly, onlyAllowIfInClass, deleteClassTeacherHandler);
|
||||
|
||||
router.use('/:classid/assignments', assignmentRouter);
|
||||
|
||||
|
|
|
@ -8,22 +8,24 @@ import {
|
|||
getGroupSubmissionsHandler,
|
||||
putGroupHandler,
|
||||
} from '../controllers/groups.js';
|
||||
import { onlyAllowIfHasAccessToGroup } from '../middleware/auth/checks/group-auth-checker.js';
|
||||
import { teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks.js';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
// Root endpoint used to search objects
|
||||
router.get('/', getAllGroupsHandler);
|
||||
router.get('/', onlyAllowIfHasAccessToAssignment, getAllGroupsHandler);
|
||||
|
||||
router.post('/', createGroupHandler);
|
||||
router.post('/', teachersOnly, onlyAllowIfHasAccessToAssignment, createGroupHandler);
|
||||
|
||||
router.get('/:groupid', getGroupHandler);
|
||||
router.get('/:groupid', onlyAllowIfHasAccessToAssignment, getGroupHandler);
|
||||
|
||||
router.put('/:groupid', putGroupHandler);
|
||||
router.put('/:groupid', teachersOnly, onlyAllowIfHasAccessToAssignment, putGroupHandler);
|
||||
|
||||
router.delete('/:groupid', deleteGroupHandler);
|
||||
router.delete('/:groupid', teachersOnly, onlyAllowIfHasAccessToAssignment, deleteGroupHandler);
|
||||
|
||||
router.get('/:groupid/submissions', getGroupSubmissionsHandler);
|
||||
router.get('/:groupid/submissions', onlyAllowIfHasAccessToGroup, getGroupSubmissionsHandler);
|
||||
|
||||
router.get('/:groupid/questions', getGroupQuestionsHandler);
|
||||
router.get('/:groupid/questions', onlyAllowIfHasAccessToGroup, getGroupQuestionsHandler);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
import express from 'express';
|
||||
import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js';
|
||||
|
||||
import {
|
||||
getAllLearningObjects,
|
||||
getAttachment,
|
||||
getLearningObject,
|
||||
getLearningObjectHTML,
|
||||
handleDeleteLearningObject,
|
||||
handlePostLearningObject,
|
||||
} from '../controllers/learning-objects.js';
|
||||
import submissionRoutes from './submissions.js';
|
||||
import questionRoutes from './questions.js';
|
||||
import fileUpload from 'express-fileupload';
|
||||
import { onlyAdminsForLearningObject } from '../middleware/auth/checks/learning-object-auth-checks.js';
|
||||
import { authenticatedOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
@ -16,13 +25,21 @@ const router = express.Router();
|
|||
|
||||
// Route 2: list of object data
|
||||
// Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie
|
||||
router.get('/', getAllLearningObjects);
|
||||
router.get('/', authenticatedOnly, getAllLearningObjects);
|
||||
|
||||
router.post('/', teachersOnly, fileUpload({ useTempFiles: true }), handlePostLearningObject);
|
||||
|
||||
// Parameter: hruid of learning object
|
||||
// Query: language
|
||||
// Route to fetch data of one learning object based on its hruid
|
||||
// Example: http://localhost:3000/learningObject/un_ai7
|
||||
router.get('/:hruid', getLearningObject);
|
||||
router.get('/:hruid', authenticatedOnly, getLearningObject);
|
||||
|
||||
// Parameter: hruid of learning object
|
||||
// Query: language
|
||||
// Route to delete a learning object based on its hruid.
|
||||
// Example: http://localhost:3000/learningObject/un_ai7?language=nl&version=1
|
||||
router.delete('/:hruid', onlyAdminsForLearningObject, handleDeleteLearningObject);
|
||||
|
||||
router.use('/:hruid/submissions', submissionRoutes);
|
||||
|
||||
|
@ -32,12 +49,12 @@ router.use('/:hruid/:version/questions', questionRoutes);
|
|||
// Query: language, version (optional)
|
||||
// Route to fetch the HTML rendering of one learning object based on its hruid.
|
||||
// Example: http://localhost:3000/learningObject/un_ai7/html
|
||||
router.get('/:hruid/html', getLearningObjectHTML);
|
||||
router.get('/:hruid/html', authenticatedOnly, getLearningObjectHTML);
|
||||
|
||||
// Parameter: hruid of learning object, name of attachment.
|
||||
// Query: language, version (optional).
|
||||
// Route to get the raw data of the attachment for one learning object based on its hruid.
|
||||
// Example: http://localhost:3000/learningObject/u_test/attachment/testimage.png
|
||||
router.get('/:hruid/html/:attachmentName', getAttachment);
|
||||
router.get('/:hruid/html/:attachmentName', authenticatedOnly, getAttachment);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import express from 'express';
|
||||
import { getLearningPaths } from '../controllers/learning-paths.js';
|
||||
import { authenticatedOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
import { deleteLearningPath, getLearningPaths, postLearningPath, putLearningPath } from '../controllers/learning-paths.js';
|
||||
import { onlyAdminsForLearningPath } from '../middleware/auth/checks/learning-path-auth-checks.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
@ -22,6 +24,10 @@ const router = express.Router();
|
|||
// Route to fetch learning paths based on a theme
|
||||
// Example: http://localhost:3000/learningPath?theme=kiks
|
||||
|
||||
router.get('/', getLearningPaths);
|
||||
router.get('/', authenticatedOnly, getLearningPaths);
|
||||
router.post('/', teachersOnly, postLearningPath);
|
||||
|
||||
router.put('/:hruid/:language', onlyAdminsForLearningPath, putLearningPath);
|
||||
router.delete('/:hruid/:language', onlyAdminsForLearningPath, deleteLearningPath);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
import express from 'express';
|
||||
import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js';
|
||||
import answerRoutes from './answers.js';
|
||||
import { authenticatedOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
import { updateAnswerHandler } from '../controllers/answers.js';
|
||||
import { onlyAllowAuthor, onlyAllowAuthorRequest, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
// Query language
|
||||
|
||||
// Root endpoint used to search objects
|
||||
router.get('/', getAllQuestionsHandler);
|
||||
router.get('/', authenticatedOnly, getAllQuestionsHandler);
|
||||
|
||||
router.post('/', createQuestionHandler);
|
||||
|
||||
router.delete('/:seq', deleteQuestionHandler);
|
||||
router.post('/', studentsOnly, onlyAllowAuthor, createQuestionHandler);
|
||||
|
||||
// Information about a question with id
|
||||
router.get('/:seq', getQuestionHandler);
|
||||
router.get('/:seq', onlyAllowIfHasAccessToQuestion, getQuestionHandler);
|
||||
|
||||
router.delete('/:seq', studentsOnly, onlyAllowAuthorRequest, deleteQuestionHandler);
|
||||
|
||||
router.put('/:seq', studentsOnly, onlyAllowAuthorRequest, updateAnswerHandler);
|
||||
|
||||
router.use('/:seq/answers', answerRoutes);
|
||||
|
||||
|
|
|
@ -18,12 +18,30 @@ router.get('/', (_, res: Response) => {
|
|||
});
|
||||
});
|
||||
|
||||
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
|
||||
router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */);
|
||||
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
|
||||
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
|
||||
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
|
||||
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);
|
||||
router.use('/learningObject', learningObjectRoutes /* #swagger.tags = ['Learning Object'] */);
|
||||
router.use(
|
||||
'/class',
|
||||
classRouter /* #swagger.tags = ['Class'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||
);
|
||||
router.use(
|
||||
'/learningObject',
|
||||
learningObjectRoutes /* #swagger.tags = ['Learning Object'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||
);
|
||||
router.use(
|
||||
'/learningPath',
|
||||
learningPathRoutes /* #swagger.tags = ['Learning Path'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||
);
|
||||
router.use(
|
||||
'/student',
|
||||
studentRouter /* #swagger.tags = ['Student'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||
);
|
||||
router.use(
|
||||
'/teacher',
|
||||
teacherRouter /* #swagger.tags = ['Teacher'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||
);
|
||||
router.use(
|
||||
'/theme',
|
||||
themeRoutes /* #swagger.tags = ['Theme'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -5,15 +5,19 @@ import {
|
|||
getStudentRequestHandler,
|
||||
getStudentRequestsHandler,
|
||||
} from '../controllers/students.js';
|
||||
import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js';
|
||||
import { onlyAllowStudentHimselfAndTeachersOfClass } from '../middleware/auth/checks/class-auth-checks.js';
|
||||
|
||||
// Under /:username/joinRequests/
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get('/', getStudentRequestsHandler);
|
||||
router.get('/', preventImpersonation, getStudentRequestsHandler);
|
||||
|
||||
router.post('/', createStudentRequestHandler);
|
||||
router.post('/', preventImpersonation, createStudentRequestHandler);
|
||||
|
||||
router.get('/:classId', getStudentRequestHandler);
|
||||
router.get('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, getStudentRequestHandler);
|
||||
|
||||
router.delete('/:classId', deleteClassJoinRequestHandler);
|
||||
router.delete('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, deleteClassJoinRequestHandler);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -11,33 +11,37 @@ import {
|
|||
getStudentSubmissionsHandler,
|
||||
} from '../controllers/students.js';
|
||||
import joinRequestRouter from './student-join-requests.js';
|
||||
import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js';
|
||||
import { adminOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Root endpoint used to search objects
|
||||
router.get('/', getAllStudentsHandler);
|
||||
router.get('/', adminOnly, getAllStudentsHandler);
|
||||
|
||||
router.post('/', createStudentHandler);
|
||||
// Users will be created automatically when some resource is created for them. Therefore, this endpoint
|
||||
// Can only be used by an administrator.
|
||||
router.post('/', adminOnly, createStudentHandler);
|
||||
|
||||
router.delete('/:username', deleteStudentHandler);
|
||||
router.delete('/:username', preventImpersonation, deleteStudentHandler);
|
||||
|
||||
// Information about a student's profile
|
||||
router.get('/:username', getStudentHandler);
|
||||
router.get('/:username', preventImpersonation, getStudentHandler);
|
||||
|
||||
// The list of classes a student is in
|
||||
router.get('/:username/classes', getStudentClassesHandler);
|
||||
router.get('/:username/classes', preventImpersonation, getStudentClassesHandler);
|
||||
|
||||
// The list of submissions a student has made
|
||||
router.get('/:username/submissions', getStudentSubmissionsHandler);
|
||||
router.get('/:username/submissions', preventImpersonation, getStudentSubmissionsHandler);
|
||||
|
||||
// The list of assignments a student has
|
||||
router.get('/:username/assignments', getStudentAssignmentsHandler);
|
||||
router.get('/:username/assignments', preventImpersonation, getStudentAssignmentsHandler);
|
||||
|
||||
// The list of groups a student is in
|
||||
router.get('/:username/groups', getStudentGroupsHandler);
|
||||
router.get('/:username/groups', preventImpersonation, getStudentGroupsHandler);
|
||||
|
||||
// A list of questions a user has created
|
||||
router.get('/:username/questions', getStudentQuestionsHandler);
|
||||
router.get('/:username/questions', preventImpersonation, getStudentQuestionsHandler);
|
||||
|
||||
router.use('/:username/joinRequests', joinRequestRouter);
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import express from 'express';
|
||||
import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.js';
|
||||
import { onlyAllowIfHasAccessToSubmission, onlyAllowSubmitter } from '../middleware/auth/checks/submission-checks.js';
|
||||
import { adminOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
// Root endpoint used to search objects
|
||||
router.get('/', getSubmissionsHandler);
|
||||
router.get('/', adminOnly, getSubmissionsHandler);
|
||||
|
||||
router.post('/', createSubmissionHandler);
|
||||
router.post('/', studentsOnly, onlyAllowSubmitter, createSubmissionHandler);
|
||||
|
||||
// Information about an submission with id 'id'
|
||||
router.get('/:id', getSubmissionHandler);
|
||||
router.get('/:id', onlyAllowIfHasAccessToSubmission, getSubmissionHandler);
|
||||
|
||||
router.delete('/:id', deleteSubmissionHandler);
|
||||
router.delete('/:id', onlyAllowIfHasAccessToSubmission, deleteSubmissionHandler);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -6,17 +6,24 @@ import {
|
|||
getInvitationHandler,
|
||||
updateInvitationHandler,
|
||||
} from '../controllers/teacher-invitations.js';
|
||||
import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js';
|
||||
import {
|
||||
onlyAllowReceiverBody,
|
||||
onlyAllowSender,
|
||||
onlyAllowSenderBody,
|
||||
onlyAllowSenderOrReceiver,
|
||||
} from '../middleware/auth/checks/teacher-invitation-checks.js';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get('/:username', getAllInvitationsHandler);
|
||||
router.get('/:username', preventImpersonation, getAllInvitationsHandler);
|
||||
|
||||
router.get('/:sender/:receiver/:classId', getInvitationHandler);
|
||||
router.get('/:sender/:receiver/:classId', onlyAllowSenderOrReceiver, getInvitationHandler);
|
||||
|
||||
router.post('/', createInvitationHandler);
|
||||
router.post('/', onlyAllowSenderBody, createInvitationHandler);
|
||||
|
||||
router.put('/', updateInvitationHandler);
|
||||
router.put('/', onlyAllowReceiverBody, updateInvitationHandler);
|
||||
|
||||
router.delete('/:sender/:receiver/:classId', deleteInvitationHandler);
|
||||
router.delete('/:sender/:receiver/:classId', onlyAllowSender, deleteInvitationHandler);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -6,32 +6,31 @@ import {
|
|||
getStudentJoinRequestHandler,
|
||||
getTeacherClassHandler,
|
||||
getTeacherHandler,
|
||||
getTeacherQuestionHandler,
|
||||
getTeacherStudentHandler,
|
||||
updateStudentJoinRequestHandler,
|
||||
} from '../controllers/teachers.js';
|
||||
import invitationRouter from './teacher-invitations.js';
|
||||
|
||||
import { adminOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js';
|
||||
import { onlyAllowTeacherOfClass } from '../middleware/auth/checks/class-auth-checks.js';
|
||||
const router = express.Router();
|
||||
|
||||
// Root endpoint used to search objects
|
||||
router.get('/', getAllTeachersHandler);
|
||||
router.get('/', adminOnly, getAllTeachersHandler);
|
||||
|
||||
router.post('/', createTeacherHandler);
|
||||
router.post('/', adminOnly, createTeacherHandler);
|
||||
|
||||
router.get('/:username', getTeacherHandler);
|
||||
router.get('/:username', preventImpersonation, getTeacherHandler);
|
||||
|
||||
router.delete('/:username', deleteTeacherHandler);
|
||||
router.delete('/:username', preventImpersonation, deleteTeacherHandler);
|
||||
|
||||
router.get('/:username/classes', getTeacherClassHandler);
|
||||
router.get('/:username/classes', preventImpersonation, getTeacherClassHandler);
|
||||
|
||||
router.get('/:username/students', getTeacherStudentHandler);
|
||||
router.get('/:username/students', preventImpersonation, getTeacherStudentHandler);
|
||||
|
||||
router.get('/:username/questions', getTeacherQuestionHandler);
|
||||
router.get('/:username/joinRequests/:classId', onlyAllowTeacherOfClass, getStudentJoinRequestHandler);
|
||||
|
||||
router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler);
|
||||
|
||||
router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler);
|
||||
router.put('/:username/joinRequests/:classId/:studentUsername', onlyAllowTeacherOfClass, updateStudentJoinRequestHandler);
|
||||
|
||||
// Invitations to other classes a teacher received
|
||||
router.use('/invitations', invitationRouter);
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import express from 'express';
|
||||
import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js';
|
||||
import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Query: language
|
||||
// Route to fetch list of {key, title, description, image} themes in their respective language
|
||||
router.get('/', getThemesHandler);
|
||||
router.get('/', authenticatedOnly, getThemesHandler);
|
||||
|
||||
// Arg: theme (key)
|
||||
// Route to fetch list of hruids based on theme
|
||||
router.get('/:theme', getHruidsByThemeHandler);
|
||||
router.get('/:theme', authenticatedOnly, getHruidsByThemeHandler);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -34,7 +34,7 @@ export async function createAnswer(questionId: QuestionId, answerData: AnswerDat
|
|||
return mapToAnswerDTO(answer);
|
||||
}
|
||||
|
||||
async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> {
|
||||
export async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> {
|
||||
const answerRepository = getAnswerRepository();
|
||||
const question = await fetchQuestion(questionId);
|
||||
const answer = await answerRepository.findAnswer(question, sequenceNumber);
|
||||
|
|
|
@ -34,6 +34,15 @@ export async function fetchGroup(classId: string, assignmentNumber: number, grou
|
|||
return group;
|
||||
}
|
||||
|
||||
export async function fetchAllGroups(classId: string, assignmentNumber: number): Promise<Group[]> {
|
||||
const assignment = await fetchAssignment(classId, assignmentNumber);
|
||||
|
||||
const groupRepository = getGroupRepository();
|
||||
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> {
|
||||
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
|
||||
return mapToGroupDTO(group, group.assignment.within);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { DWENGO_API_BASE } from '../config.js';
|
||||
import { fetchWithLogging } from '../util/api-helper.js';
|
||||
import { fetchRemote } from '../util/api-helper.js';
|
||||
|
||||
import {
|
||||
FilteredLearningObject,
|
||||
|
@ -39,10 +39,7 @@ function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLear
|
|||
*/
|
||||
export async function getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> {
|
||||
const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`;
|
||||
const metadata = await fetchWithLogging<LearningObjectMetadata>(
|
||||
metadataUrl,
|
||||
`Metadata for Learning Object HRUID "${hruid}" (language ${language})`
|
||||
);
|
||||
const metadata = await fetchRemote<LearningObjectMetadata>(metadataUrl, `Metadata for Learning Object HRUID "${hruid}" (language ${language})`);
|
||||
|
||||
if (!metadata) {
|
||||
getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`);
|
||||
|
|
|
@ -109,6 +109,15 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
|
|||
);
|
||||
return learningObjects.filter((it) => it !== null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns all learning objects containing the given username as an admin.
|
||||
*/
|
||||
async getLearningObjectsAdministratedBy(adminUsername: string): Promise<FilteredLearningObject[]> {
|
||||
const learningObjectRepo = getLearningObjectRepository();
|
||||
const learningObjects = await learningObjectRepo.findAllByAdmin(adminUsername);
|
||||
return learningObjects.map((it) => convertLearningObject(it)).filter((it) => it !== null);
|
||||
},
|
||||
};
|
||||
|
||||
export default databaseLearningObjectProvider;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { DWENGO_API_BASE } from '../../config.js';
|
||||
import { fetchWithLogging } from '../../util/api-helper.js';
|
||||
import { fetchRemote } from '../../util/api-helper.js';
|
||||
import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js';
|
||||
import { LearningObjectProvider } from './learning-object-provider.js';
|
||||
import { getLogger, Logger } from '../../logging/initalize.js';
|
||||
|
@ -88,7 +88,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
|
|||
*/
|
||||
async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> {
|
||||
const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`;
|
||||
const metadata = await fetchWithLogging<LearningObjectMetadata>(
|
||||
const metadata = await fetchRemote<LearningObjectMetadata>(
|
||||
metadataUrl,
|
||||
`Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`,
|
||||
{
|
||||
|
@ -124,7 +124,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
|
|||
*/
|
||||
async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> {
|
||||
const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`;
|
||||
const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, {
|
||||
const html = await fetchRemote<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, {
|
||||
params: { ...id },
|
||||
});
|
||||
|
||||
|
@ -135,6 +135,13 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
|
|||
|
||||
return html;
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtain all learning objects who have the user with the given username as an admin.
|
||||
*/
|
||||
async getLearningObjectsAdministratedBy(_adminUsername: string): Promise<FilteredLearningObject[]> {
|
||||
return []; // The dwengo database does not contain any learning objects administrated by users.
|
||||
},
|
||||
};
|
||||
|
||||
export default dwengoApiLearningObjectProvider;
|
||||
|
|
|
@ -20,4 +20,9 @@ export interface LearningObjectProvider {
|
|||
* Obtain a HTML-rendering of the learning object with the given identifier (as a string).
|
||||
*/
|
||||
getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* Obtain all learning object who have the user with the given username as an admin.
|
||||
*/
|
||||
getLearningObjectsAdministratedBy(username: string): Promise<FilteredLearningObject[]>;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,11 @@ import { LearningObjectProvider } from './learning-object-provider.js';
|
|||
import { envVars, getEnvVar } from '../../util/envVars.js';
|
||||
import databaseLearningObjectProvider from './database-learning-object-provider.js';
|
||||
import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
|
||||
import { getLearningObjectRepository, getTeacherRepository } from '../../data/repositories.js';
|
||||
import { processLearningObjectZip } from './learning-object-zip-processing-service.js';
|
||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||
import { NotFoundException } from '../../exceptions/not-found-exception.js';
|
||||
|
||||
function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider {
|
||||
if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
|
||||
|
@ -42,6 +47,66 @@ const learningObjectService = {
|
|||
async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> {
|
||||
return getProvider(id).getLearningObjectHTML(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtain all learning objects administrated by the user with the given username.
|
||||
*/
|
||||
async getLearningObjectsAdministratedBy(adminUsername: string): Promise<FilteredLearningObject[]> {
|
||||
return databaseLearningObjectProvider.getLearningObjectsAdministratedBy(adminUsername);
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the learning object in the given zip file in the database.
|
||||
* @param learningObjectPath The path where the uploaded learning object resides.
|
||||
* @param admins The usernames of the users which should be administrators of the learning object.
|
||||
*/
|
||||
async storeLearningObject(learningObjectPath: string, admins: string[]): Promise<LearningObject> {
|
||||
const learningObjectRepository = getLearningObjectRepository();
|
||||
const learningObject = await processLearningObjectZip(learningObjectPath);
|
||||
|
||||
if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
|
||||
learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid;
|
||||
}
|
||||
|
||||
// Lookup the admin teachers based on their usernames and add them to the admins of the learning object.
|
||||
const teacherRepo = getTeacherRepository();
|
||||
const adminTeachers = await Promise.all(admins.map(async (it) => teacherRepo.findByUsername(it)));
|
||||
adminTeachers.forEach((it) => {
|
||||
if (it !== null) {
|
||||
learningObject.admins.add(it);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await learningObjectRepository.save(learningObject, { preventOverwrite: true });
|
||||
} catch (e: unknown) {
|
||||
learningObjectRepository.getEntityManager().clear();
|
||||
throw e;
|
||||
}
|
||||
|
||||
return learningObject;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the learning object with the given identifier.
|
||||
*/
|
||||
async deleteLearningObject(id: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||
const learningObjectRepository = getLearningObjectRepository();
|
||||
return await learningObjectRepository.removeByIdentifier(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of the usernames of the administrators of the learning object with the given identifier.
|
||||
* @throws NotFoundException if the specified learning object was not found in the database.
|
||||
*/
|
||||
async getAdmins(id: LearningObjectIdentifier): Promise<string[]> {
|
||||
const learningObjectRepo = getLearningObjectRepository();
|
||||
const learningObject = await learningObjectRepo.findByIdentifier(id);
|
||||
if (!learningObject) {
|
||||
throw new NotFoundException('learningObjectNotFound');
|
||||
}
|
||||
return learningObject.admins.map((admin) => admin.username);
|
||||
},
|
||||
};
|
||||
|
||||
export default learningObjectService;
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
import unzipper from 'unzipper';
|
||||
import mime from 'mime-types';
|
||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||
import { getAttachmentRepository, getLearningObjectRepository } from '../../data/repositories.js';
|
||||
import { BadRequestException } from '../../exceptions/bad-request-exception.js';
|
||||
import { LearningObjectMetadata } from '@dwengo-1/common/interfaces/learning-content';
|
||||
import { DwengoContentType } from './processing/content-type.js';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const METADATA_PATH_REGEX = /.*[/^]metadata\.json$/;
|
||||
const CONTENT_PATH_REGEX = /.*[/^]content\.[a-zA-Z]*$/;
|
||||
|
||||
/**
|
||||
* Process an uploaded zip file and construct a LearningObject from its contents.
|
||||
* @param filePath Path of the zip file to process.
|
||||
*/
|
||||
export async function processLearningObjectZip(filePath: string): Promise<LearningObject> {
|
||||
let zip: unzipper.CentralDirectory;
|
||||
try {
|
||||
zip = await unzipper.Open.file(filePath);
|
||||
} catch (_: unknown) {
|
||||
throw new BadRequestException('invalidZip');
|
||||
}
|
||||
|
||||
let metadata: LearningObjectMetadata | undefined = undefined;
|
||||
const attachments: { name: string; content: Buffer }[] = [];
|
||||
let content: Buffer | undefined = undefined;
|
||||
|
||||
if (zip.files.length === 0) {
|
||||
throw new BadRequestException('emptyZip');
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
zip.files.map(async (file) => {
|
||||
if (file.type !== 'Directory') {
|
||||
if (METADATA_PATH_REGEX.test(file.path)) {
|
||||
metadata = await processMetadataJson(file);
|
||||
} else if (CONTENT_PATH_REGEX.test(file.path)) {
|
||||
content = await processFile(file);
|
||||
} else {
|
||||
attachments.push({
|
||||
name: file.path,
|
||||
content: await processFile(file),
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (!metadata) {
|
||||
throw new BadRequestException('missingMetadata');
|
||||
}
|
||||
if (!content) {
|
||||
throw new BadRequestException('missingIndex');
|
||||
}
|
||||
|
||||
const learningObject = createLearningObject(metadata, content, attachments);
|
||||
|
||||
return learningObject;
|
||||
}
|
||||
|
||||
function createLearningObject(metadata: LearningObjectMetadata, content: Buffer, attachments: { name: string; content: Buffer }[]): LearningObject {
|
||||
const learningObjectRepo = getLearningObjectRepository();
|
||||
const attachmentRepo = getAttachmentRepository();
|
||||
|
||||
const returnValue = {
|
||||
callbackUrl: metadata.return_value?.callback_url ?? '',
|
||||
callbackSchema: metadata.return_value?.callback_schema ? JSON.stringify(metadata.return_value.callback_schema) : '',
|
||||
};
|
||||
|
||||
if (!metadata.target_ages || metadata.target_ages.length === 0) {
|
||||
throw new BadRequestException('targetAgesMandatory');
|
||||
}
|
||||
|
||||
const learningObject = learningObjectRepo.create({
|
||||
admins: [],
|
||||
available: metadata.available ?? true,
|
||||
content: content,
|
||||
contentType: metadata.content_type as DwengoContentType,
|
||||
copyright: metadata.copyright ?? '',
|
||||
description: metadata.description ?? '',
|
||||
educationalGoals: metadata.educational_goals ?? [],
|
||||
hruid: metadata.hruid,
|
||||
keywords: metadata.keywords,
|
||||
language: metadata.language,
|
||||
license: metadata.license ?? '',
|
||||
returnValue,
|
||||
skosConcepts: metadata.skos_concepts ?? [],
|
||||
teacherExclusive: metadata.teacher_exclusive,
|
||||
title: metadata.title,
|
||||
version: metadata.version,
|
||||
estimatedTime: metadata.estimated_time ?? 1,
|
||||
targetAges: metadata.target_ages ?? [],
|
||||
difficulty: metadata.difficulty ?? 1,
|
||||
uuid: v4(),
|
||||
});
|
||||
const attachmentEntities = attachments.map((it) =>
|
||||
attachmentRepo.create({
|
||||
name: it.name,
|
||||
content: it.content,
|
||||
mimeType: mime.lookup(it.name) || 'text/plain',
|
||||
learningObject,
|
||||
})
|
||||
);
|
||||
attachmentEntities.forEach((it) => {
|
||||
learningObject.attachments.add(it);
|
||||
});
|
||||
return learningObject;
|
||||
}
|
||||
|
||||
async function processMetadataJson(file: unzipper.File): Promise<LearningObjectMetadata> {
|
||||
const buf = await file.buffer();
|
||||
const content = buf.toString();
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
async function processFile(file: unzipper.File): Promise<Buffer> {
|
||||
return await file.buffer();
|
||||
}
|
|
@ -4,7 +4,7 @@ import { getLearningPathRepository } from '../../data/repositories.js';
|
|||
import learningObjectService from '../learning-objects/learning-object-service.js';
|
||||
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
|
||||
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||
import { getLastSubmissionForGroup, isTransitionPossible } from './learning-path-personalization-util.js';
|
||||
import { getLastSubmissionForGroup, idFromLearningPathNode, isTransitionPossible } from './learning-path-personalization-util.js';
|
||||
import {
|
||||
FilteredLearningObject,
|
||||
LearningObjectNode,
|
||||
|
@ -17,8 +17,11 @@ import { MatchMode } from '@dwengo-1/common/util/match-mode';
|
|||
import { Group } from '../../entities/assignments/group.entity';
|
||||
import { Collection } from '@mikro-orm/core';
|
||||
import { v4 } from 'uuid';
|
||||
import { getLogger } from '../../logging/initalize.js';
|
||||
import { Teacher } from '../../entities/users/teacher.entity';
|
||||
|
||||
const logger = getLogger();
|
||||
|
||||
/**
|
||||
* Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its
|
||||
* corresponding learning object.
|
||||
|
@ -40,8 +43,13 @@ async function getLearningObjectsForNodes(nodes: Collection<LearningPathNode>):
|
|||
),
|
||||
),
|
||||
);
|
||||
if (Array.from(nullableNodesToLearningObjects.values()).some((it) => it === null)) {
|
||||
throw new Error('At least one of the learning objects on this path could not be found.');
|
||||
|
||||
// Ignore all learning objects that cannot be found such that the rest of the learning path keeps working.
|
||||
for (const [key, value] of nullableNodesToLearningObjects) {
|
||||
if (value === null) {
|
||||
logger.warn(`Learning object ${key.learningObjectHruid}/${key.language}/${key.version} not found!`);
|
||||
nullableNodesToLearningObjects.delete(key);
|
||||
}
|
||||
}
|
||||
return nullableNodesToLearningObjects as Map<LearningPathNode, FilteredLearningObject>;
|
||||
}
|
||||
|
@ -97,14 +105,22 @@ async function convertNode(
|
|||
personalizedFor: Group | undefined,
|
||||
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
|
||||
): Promise<LearningObjectNode> {
|
||||
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(node, personalizedFor) : null;
|
||||
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningPathNode(node), personalizedFor) : null;
|
||||
const transitions = node.transitions
|
||||
.filter(
|
||||
(trans) =>
|
||||
!personalizedFor || // If we do not want a personalized learning path, keep all transitions
|
||||
isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)), // Otherwise remove all transitions that aren't possible.
|
||||
)
|
||||
.map((trans, i) => convertTransition(trans, i, nodesToLearningObjects));
|
||||
.map((trans, i) => {
|
||||
try {
|
||||
return convertTransition(trans, i, nodesToLearningObjects);
|
||||
} catch (_: unknown) {
|
||||
logger.error(`Transition could not be resolved: ${JSON.stringify(trans)}`);
|
||||
return undefined; // Do not crash on invalid transitions, just ignore them so the rest of the learning path keeps working.
|
||||
}
|
||||
})
|
||||
.filter((it) => it !== undefined);
|
||||
return {
|
||||
_id: learningObject.uuid,
|
||||
language: learningObject.language,
|
||||
|
@ -166,6 +182,7 @@ function convertTransition(
|
|||
return {
|
||||
_id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
|
||||
default: false, // We don't work with default transitions but retain this for backwards compatibility.
|
||||
condition: transition.condition,
|
||||
next: {
|
||||
_id: nextNode._id ? nextNode._id + index : v4(), // Construct a unique ID for the transition for backwards compatibility.
|
||||
hruid: transition.next.learningObjectHruid,
|
||||
|
@ -200,6 +217,15 @@ const databaseLearningPathProvider: LearningPathProvider = {
|
|||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns all the learning paths which have the user with the given username as an administrator.
|
||||
*/
|
||||
async getLearningPathsAdministratedBy(adminUsername: string): Promise<LearningPath[]> {
|
||||
const repo = getLearningPathRepository();
|
||||
const paths = await repo.findAllByAdminUsername(adminUsername);
|
||||
return await Promise.all(paths.map(async (result, index) => convertLearningPath(result, index)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Search learning paths in the database using the given search string.
|
||||
*/
|
||||
|
|
|
@ -1,15 +1,37 @@
|
|||
import { fetchWithLogging } from '../../util/api-helper.js';
|
||||
import { fetchRemote } from '../../util/api-helper.js';
|
||||
import { DWENGO_API_BASE } from '../../config.js';
|
||||
import { LearningPathProvider } from './learning-path-provider.js';
|
||||
import { getLogger, Logger } from '../../logging/initalize.js';
|
||||
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
|
||||
import { Teacher } from '../../entities/users/teacher.entity';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { Group } from '../../entities/assignments/group.entity.js';
|
||||
import { getLastSubmissionForGroup, idFromLearningObjectNode } from './learning-path-personalization-util.js';
|
||||
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
/**
|
||||
* Adds progress information to the learning path. Modifies the learning path in-place.
|
||||
* @param learningPath The learning path to add progress to.
|
||||
* @param personalizedFor The group whose progress should be shown.
|
||||
* @returns the modified learning path.
|
||||
*/
|
||||
async function addProgressToLearningPath(learningPath: LearningPath, personalizedFor: Group): Promise<LearningPath> {
|
||||
await Promise.all(
|
||||
learningPath.nodes.map(async (node) => {
|
||||
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningObjectNode(node), personalizedFor) : null;
|
||||
node.done = Boolean(lastSubmission);
|
||||
})
|
||||
);
|
||||
|
||||
learningPath.num_nodes = learningPath.nodes.length;
|
||||
learningPath.num_nodes_left = learningPath.nodes.filter((it) => !it.done).length;
|
||||
|
||||
return learningPath;
|
||||
}
|
||||
|
||||
const dwengoApiLearningPathProvider: LearningPathProvider = {
|
||||
async fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse> {
|
||||
async fetchLearningPaths(hruids: string[], language: string, source: string, personalizedFor: Group): Promise<LearningPathResponse> {
|
||||
if (hruids.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
|
@ -22,7 +44,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = {
|
|||
const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`;
|
||||
const params = { pathIdList: JSON.stringify({ hruids }), language };
|
||||
|
||||
const learningPaths = await fetchWithLogging<LearningPath[]>(apiUrl, `Learning paths for ${source}`, { params });
|
||||
const learningPaths = await fetchRemote<LearningPath[]>(apiUrl, `Learning paths for ${source}`, { params });
|
||||
|
||||
if (!learningPaths || learningPaths.length === 0) {
|
||||
logger.warn(`⚠️ WARNING: No learning paths found for ${source}.`);
|
||||
|
@ -34,6 +56,8 @@ const dwengoApiLearningPathProvider: LearningPathProvider = {
|
|||
};
|
||||
}
|
||||
|
||||
await Promise.all(learningPaths?.map(async (it) => addProgressToLearningPath(it, personalizedFor)));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
source,
|
||||
|
@ -41,14 +65,22 @@ const dwengoApiLearningPathProvider: LearningPathProvider = {
|
|||
};
|
||||
},
|
||||
|
||||
async searchLearningPaths(query: string, language: string): Promise<LearningPath[]> {
|
||||
async searchLearningPaths(query: string, language: string, personalizedFor: Group): Promise<LearningPath[]> {
|
||||
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
|
||||
const params = { all: query, language };
|
||||
|
||||
const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params });
|
||||
const searchResults = await fetchRemote<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params });
|
||||
|
||||
if (searchResults) {
|
||||
await Promise.all(searchResults?.map(async (it) => addProgressToLearningPath(it, personalizedFor)));
|
||||
}
|
||||
return searchResults ?? [];
|
||||
},
|
||||
|
||||
async getLearningPathsAdministratedBy(_adminUsername: string) {
|
||||
return []; // Learning paths fetched from the Dwengo API cannot be administrated by a user.
|
||||
},
|
||||
|
||||
async searchLearningPathsByAdmin(admins: Teacher[], language: string): Promise<LearningPath[]> {
|
||||
if (!admins || admins.length === 0) {
|
||||
return this.searchLearningPaths('', language as Language);
|
||||
|
|
|
@ -5,18 +5,36 @@ import { getSubmissionRepository } from '../../data/repositories.js';
|
|||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import { LearningObjectNode } from '@dwengo-1/common/interfaces/learning-content';
|
||||
|
||||
/**
|
||||
* Returns the last submission for the learning object associated with the given node and for the group
|
||||
*/
|
||||
export async function getLastSubmissionForGroup(node: LearningPathNode, pathFor: Group): Promise<Submission | null> {
|
||||
export async function getLastSubmissionForGroup(learningObjectId: LearningObjectIdentifier, pathFor: Group): Promise<Submission | null> {
|
||||
const submissionRepo = getSubmissionRepository();
|
||||
const learningObjectId: LearningObjectIdentifier = {
|
||||
return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a LearningObjectIdentifier describing the specified node.
|
||||
*/
|
||||
export function idFromLearningObjectNode(node: LearningObjectNode): LearningObjectIdentifier {
|
||||
return {
|
||||
hruid: node.learningobject_hruid,
|
||||
language: node.language,
|
||||
version: node.version,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a LearningObjectIdentifier describing the specified node.
|
||||
*/
|
||||
export function idFromLearningPathNode(node: LearningPathNode): LearningObjectIdentifier {
|
||||
return {
|
||||
hruid: node.learningObjectHruid,
|
||||
language: node.language,
|
||||
version: node.version,
|
||||
};
|
||||
return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,6 +18,11 @@ export interface LearningPathProvider {
|
|||
*/
|
||||
searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]>;
|
||||
|
||||
/**
|
||||
* Get all learning paths which have the teacher with the given user as an administrator.
|
||||
*/
|
||||
getLearningPathsAdministratedBy(adminUsername: string): Promise<LearningPath[]>;
|
||||
|
||||
/**
|
||||
* Fetch the learning paths for the given admins from the data source.
|
||||
*/
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js';
|
||||
import databaseLearningPathProvider from './database-learning-path-provider.js';
|
||||
import { envVars, getEnvVar } from '../../util/envVars.js';
|
||||
import { LearningObjectNode, LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
|
||||
import {
|
||||
LearningObjectNode,
|
||||
LearningPath,
|
||||
LearningPathIdentifier,
|
||||
LearningPathResponse,
|
||||
} from '@dwengo-1/common/interfaces/learning-content';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { Group } from '../../entities/assignments/group.entity.js';
|
||||
import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.entity.js';
|
||||
|
@ -12,6 +17,9 @@ import { base64ToArrayBuffer } from '../../util/base64-buffer-conversion.js';
|
|||
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
||||
import { mapToTeacher } from '../../interfaces/teacher.js';
|
||||
import { Collection } from '@mikro-orm/core';
|
||||
import { NotFoundException } from '../../exceptions/not-found-exception.js';
|
||||
import { BadRequestException } from '../../exceptions/bad-request-exception.js';
|
||||
import learningObjectService from '../learning-objects/learning-object-service.js';
|
||||
import { Teacher } from '../../entities/users/teacher.entity';
|
||||
|
||||
const userContentPrefix = getEnvVar(envVars.UserContentPrefix);
|
||||
|
@ -44,27 +52,24 @@ export function mapToLearningPath(dto: LearningPath, adminsDto: TeacherDTO[]): L
|
|||
const fromNode = nodes.find(
|
||||
(it) => it.learningObjectHruid === nodeDto.learningobject_hruid && it.language === nodeDto.language && it.version === nodeDto.version,
|
||||
)!;
|
||||
const transitions = nodeDto.transitions
|
||||
.map((transDto, i) => {
|
||||
const toNode = nodes.find(
|
||||
(it) =>
|
||||
it.learningObjectHruid === transDto.next.hruid &&
|
||||
it.language === transDto.next.language &&
|
||||
it.version === transDto.next.version,
|
||||
);
|
||||
const transitions = nodeDto.transitions.map((transDto, i) => {
|
||||
const toNode = nodes.find(
|
||||
(it) =>
|
||||
it.learningObjectHruid === transDto.next.hruid && it.language === transDto.next.language && it.version === transDto.next.version,
|
||||
);
|
||||
|
||||
if (toNode) {
|
||||
return repo.createTransition({
|
||||
transitionNumber: i,
|
||||
node: fromNode,
|
||||
next: toNode,
|
||||
condition: transDto.condition ?? 'true',
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter((it) => it)
|
||||
.map((it) => it!);
|
||||
if (toNode) {
|
||||
return repo.createTransition({
|
||||
transitionNumber: i,
|
||||
node: fromNode,
|
||||
next: toNode,
|
||||
condition: transDto.condition ?? 'true',
|
||||
});
|
||||
}
|
||||
throw new BadRequestException(
|
||||
`Invalid transition destination: ${JSON.stringify(transDto.next)}: This learning object does not exist in this learning path.`
|
||||
);
|
||||
});
|
||||
|
||||
fromNode.transitions = new Collection<LearningPathTransition>(fromNode, transitions);
|
||||
});
|
||||
|
@ -106,6 +111,14 @@ const learningPathService = {
|
|||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the learning paths administrated by the teacher with the given username.
|
||||
*/
|
||||
async getLearningPathsAdministratedBy(adminUsername: string): Promise<LearningPath[]> {
|
||||
const providerResponses = await Promise.all(allProviders.map(async (provider) => provider.getLearningPathsAdministratedBy(adminUsername)));
|
||||
return providerResponses.flat();
|
||||
},
|
||||
|
||||
/**
|
||||
* Search learning paths in the data source using the given search string.
|
||||
*/
|
||||
|
@ -136,11 +149,67 @@ const learningPathService = {
|
|||
* Add a new learning path to the database.
|
||||
* @param dto Learning path DTO from which the learning path will be created.
|
||||
* @param admins Teachers who should become an admin of the learning path.
|
||||
* @returns the created learning path.
|
||||
*/
|
||||
async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<void> {
|
||||
async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<LearningPathEntity> {
|
||||
const repo = getLearningPathRepository();
|
||||
|
||||
const userContentPrefix = getEnvVar(envVars.UserContentPrefix);
|
||||
if (!dto.hruid.startsWith(userContentPrefix)) {
|
||||
dto.hruid = userContentPrefix + dto.hruid;
|
||||
}
|
||||
|
||||
const path = mapToLearningPath(dto, admins);
|
||||
await repo.save(path, { preventOverwrite: true });
|
||||
|
||||
// Verify that all specified learning objects actually exist.
|
||||
const learningObjectsOnPath = await Promise.all(
|
||||
path.nodes.map(async (node) =>
|
||||
learningObjectService.getLearningObjectById({
|
||||
hruid: node.learningObjectHruid,
|
||||
language: node.language,
|
||||
version: node.version,
|
||||
})
|
||||
)
|
||||
);
|
||||
if (learningObjectsOnPath.some((it) => !it)) {
|
||||
throw new BadRequestException('pathContainsNonExistingLearningObjects');
|
||||
}
|
||||
|
||||
try {
|
||||
await repo.save(path, { preventOverwrite: true });
|
||||
} catch (e: unknown) {
|
||||
repo.getEntityManager().clear();
|
||||
throw e;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the learning path with the given identifier from the database.
|
||||
* @param id Identifier of the learning path to delete.
|
||||
* @returns the deleted learning path.
|
||||
*/
|
||||
async deleteLearningPath(id: LearningPathIdentifier): Promise<LearningPathEntity> {
|
||||
const repo = getLearningPathRepository();
|
||||
|
||||
const deletedPath = await repo.deleteByHruidAndLanguage(id.hruid, id.language);
|
||||
if (deletedPath) {
|
||||
return deletedPath;
|
||||
}
|
||||
throw new NotFoundException('No learning path with the given identifier found.');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of the usernames of the administrators of the learning path with the given identifier.
|
||||
* @param id The identifier of the learning path whose admins should be fetched.
|
||||
*/
|
||||
async getAdmins(id: LearningPathIdentifier): Promise<string[]> {
|
||||
const repo = getLearningPathRepository();
|
||||
const path = await repo.findByHruidAndLanguage(id.hruid, id.language);
|
||||
if (!path) {
|
||||
throw new NotFoundException('No learning path with the given identifier found.');
|
||||
}
|
||||
return path.admins.map((admin) => admin.username);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { fetchStudent } from './students.js';
|
|||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||
import { FALLBACK_VERSION_NUM } from '../config.js';
|
||||
import { fetchAssignment } from './assignments.js';
|
||||
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||
|
||||
export async function getQuestionsAboutLearningObjectInAssignment(
|
||||
loId: LearningObjectIdentifier,
|
||||
|
@ -99,10 +100,18 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat
|
|||
|
||||
const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber);
|
||||
|
||||
if (!inGroup) {
|
||||
throw new NotFoundException('Group with id and assignment not found');
|
||||
}
|
||||
|
||||
if (!inGroup.members.contains(author)) {
|
||||
throw new ConflictException('Author is not part of this group');
|
||||
}
|
||||
|
||||
const question = await questionRepository.createQuestion({
|
||||
loId,
|
||||
author,
|
||||
inGroup: inGroup!,
|
||||
inGroup: inGroup,
|
||||
content,
|
||||
});
|
||||
|
||||
|
|
|
@ -24,7 +24,8 @@ import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/subm
|
|||
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
||||
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||
import { Submission } from '../entities/assignments/submission.entity';
|
||||
import { Submission } from '../entities/assignments/submission.entity.js';
|
||||
import { mapToUsername } from '../interfaces/user.js';
|
||||
|
||||
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
|
||||
const studentRepository = getStudentRepository();
|
||||
|
@ -34,7 +35,7 @@ export async function getAllStudents(full: boolean): Promise<StudentDTO[] | stri
|
|||
return users.map(mapToStudentDTO);
|
||||
}
|
||||
|
||||
return users.map((user) => user.username);
|
||||
return users.map(mapToUsername);
|
||||
}
|
||||
|
||||
export async function fetchStudent(username: string): Promise<Student> {
|
||||
|
@ -42,7 +43,7 @@ export async function fetchStudent(username: string): Promise<Student> {
|
|||
const user = await studentRepository.findByUsername(username);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('Student with username not found');
|
||||
throw new NotFoundException(`Student with username ${username} not found`);
|
||||
}
|
||||
|
||||
return user;
|
||||
|
@ -64,7 +65,7 @@ export async function createStudent(userData: StudentDTO): Promise<StudentDTO> {
|
|||
const newStudent = mapToStudent(userData);
|
||||
await studentRepository.save(newStudent, { preventOverwrite: true });
|
||||
|
||||
return userData;
|
||||
return mapToStudentDTO(newStudent);
|
||||
}
|
||||
|
||||
export async function createOrUpdateStudent(userData: StudentDTO): Promise<StudentDTO> {
|
||||
|
|
|
@ -32,6 +32,10 @@ export async function createInvitation(data: TeacherInvitationData): Promise<Tea
|
|||
throw new ConflictException('The teacher sending the invite is not part of the class');
|
||||
}
|
||||
|
||||
if (cls.teachers.contains(receiver)) {
|
||||
throw new ConflictException('The teacher receiving the invite is already part of the class');
|
||||
}
|
||||
|
||||
const newInvitation = mapToInvitation(sender, receiver, cls);
|
||||
await teacherInvitationRepository.save(newInvitation, { preventOverwrite: true });
|
||||
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
import {
|
||||
getClassJoinRequestRepository,
|
||||
getClassRepository,
|
||||
getLearningObjectRepository,
|
||||
getQuestionRepository,
|
||||
getTeacherRepository,
|
||||
} from '../data/repositories.js';
|
||||
import { getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js';
|
||||
import { mapToClassDTO } from '../interfaces/class.js';
|
||||
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
|
||||
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
|
||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||
import { fetchStudent } from './students.js';
|
||||
|
@ -15,10 +8,6 @@ import { mapToStudentRequestDTO } from '../interfaces/student-request.js';
|
|||
import { TeacherRepository } from '../data/users/teacher-repository.js';
|
||||
import { ClassRepository } from '../data/classes/class-repository.js';
|
||||
import { Class } from '../entities/classes/class.entity.js';
|
||||
import { LearningObjectRepository } from '../data/content/learning-object-repository.js';
|
||||
import { LearningObject } from '../entities/content/learning-object.entity.js';
|
||||
import { QuestionRepository } from '../data/questions/question-repository.js';
|
||||
import { Question } from '../entities/questions/question.entity.js';
|
||||
import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js';
|
||||
import { Student } from '../entities/users/student.entity.js';
|
||||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||
|
@ -26,10 +15,10 @@ import { addClassStudent, fetchClass, getClassStudentsDTO } from './classes.js';
|
|||
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
||||
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
|
||||
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||
import { mapToUsername } from '../interfaces/user.js';
|
||||
|
||||
export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
|
||||
const teacherRepository: TeacherRepository = getTeacherRepository();
|
||||
|
@ -38,7 +27,7 @@ export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | stri
|
|||
if (full) {
|
||||
return users.map(mapToTeacherDTO);
|
||||
}
|
||||
return users.map((user) => user.username);
|
||||
return users.map(mapToUsername);
|
||||
}
|
||||
|
||||
export async function fetchTeacher(username: string): Promise<Teacher> {
|
||||
|
@ -57,7 +46,8 @@ export async function getTeacher(username: string): Promise<TeacherDTO> {
|
|||
return mapToTeacherDTO(user);
|
||||
}
|
||||
|
||||
export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO> {
|
||||
// TODO update parameter
|
||||
export async function createTeacher(userData: TeacherDTO, _update?: boolean): Promise<TeacherDTO> {
|
||||
const teacherRepository: TeacherRepository = getTeacherRepository();
|
||||
|
||||
const newTeacher = mapToTeacher(userData);
|
||||
|
@ -110,7 +100,9 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro
|
|||
|
||||
const classIds: string[] = classes.map((cls) => cls.id);
|
||||
|
||||
const students: StudentDTO[] = (await Promise.all(classIds.map(async (username) => await getClassStudentsDTO(username)))).flat();
|
||||
const students: StudentDTO[] = (await Promise.all(classIds.map(async (classId) => await getClassStudentsDTO(classId))))
|
||||
.flat()
|
||||
.filter((student, index, self) => self.findIndex((s) => s.username === student.username) === index);
|
||||
|
||||
if (full) {
|
||||
return students;
|
||||
|
@ -119,28 +111,6 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro
|
|||
return students.map((student) => student.username);
|
||||
}
|
||||
|
||||
export async function getTeacherQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
|
||||
const teacher: Teacher = await fetchTeacher(username);
|
||||
|
||||
// Find all learning objects that this teacher manages
|
||||
const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository();
|
||||
const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher);
|
||||
|
||||
if (!learningObjects || learningObjects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Fetch all questions related to these learning objects
|
||||
const questionRepository: QuestionRepository = getQuestionRepository();
|
||||
const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects);
|
||||
|
||||
if (full) {
|
||||
return questions.map(mapToQuestionDTO);
|
||||
}
|
||||
|
||||
return questions.map(mapToQuestionDTOId);
|
||||
}
|
||||
|
||||
export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> {
|
||||
const classRepository: ClassRepository = getClassRepository();
|
||||
const cls: Class | null = await classRepository.findById(classId);
|
||||
|
|
|
@ -1,28 +1,66 @@
|
|||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { getLogger, Logger } from '../logging/initalize.js';
|
||||
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||
import { getCacheClient } from '../caching.js';
|
||||
import { envVars, getEnvVar, getNumericEnvVar } from './envVars.js';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
const cacheClient = await getCacheClient();
|
||||
const logger: Logger = getLogger();
|
||||
const runMode: string = getEnvVar(envVars.RunMode);
|
||||
const prefix: string = getEnvVar(envVars.CacheKeyPrefix);
|
||||
|
||||
interface Options {
|
||||
params?: Record<string, unknown> | LearningObjectIdentifier;
|
||||
query?: Record<string, unknown>;
|
||||
responseType?: 'json' | 'text';
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to fetch data from an API endpoint with error handling.
|
||||
* Utility function to fetch data from an API endpoint with error handling and caching.
|
||||
* Logs errors but does NOT throw exceptions to keep the system running.
|
||||
*
|
||||
* @param url The API endpoint to fetch from.
|
||||
* @param description A short description of what is being fetched (for logging).
|
||||
* @param options Contains further options such as params (the query params) and responseType (whether the response
|
||||
* should be parsed as JSON ("json") or whether it should be returned as plain text ("text")
|
||||
* @param cacheTTL Time-to-live for the cache in seconds (default: 60 seconds).
|
||||
* @returns The response data if successful, or null if an error occurs.
|
||||
*/
|
||||
export async function fetchWithLogging<T>(
|
||||
url: string,
|
||||
description: string,
|
||||
options?: {
|
||||
params?: Record<string, unknown> | LearningObjectIdentifier;
|
||||
query?: Record<string, unknown>;
|
||||
responseType?: 'json' | 'text';
|
||||
export async function fetchRemote<T>(url: string, description: string, options?: Options, cacheTTL?: number): Promise<T | null> {
|
||||
if (runMode !== 'dev' && !runMode.includes('test') && cacheClient !== undefined) {
|
||||
return fetchWithCache<T>(url, description, options, cacheTTL);
|
||||
}
|
||||
): Promise<T | null> {
|
||||
|
||||
getLogger().debug(`🔄 INFO: Bypassing cache for ${description} at "${url}".`);
|
||||
return fetchWithLogging(url, description, options);
|
||||
}
|
||||
|
||||
async function fetchWithCache<T>(url: string, description: string, options?: Options, cacheTTL?: number): Promise<T | null> {
|
||||
// Combine the URL and parameters to create a unique cache key.
|
||||
// NOTE Using a hash function to keep the key short, since Memcached has a limit on key size
|
||||
const urlWithParams = `${url}${options?.params ? JSON.stringify(options.params) : ''}`;
|
||||
const hashedUrl = createHash('sha256').update(urlWithParams).digest('hex');
|
||||
const key = `${prefix}:${hashedUrl}`;
|
||||
|
||||
const cachedData = await cacheClient.get(key);
|
||||
|
||||
if (cachedData?.value) {
|
||||
logger.debug(`✅ INFO: Cache hit for ${description} at "${url}" (key: "${key}")`);
|
||||
return JSON.parse(cachedData.value.toString()) as T;
|
||||
}
|
||||
|
||||
logger.debug(`🔄 INFO: Cache miss for ${description} at "${url}". Fetching data...`);
|
||||
const response = await fetchWithLogging<T>(url, description, options);
|
||||
|
||||
const ttl = cacheTTL || getNumericEnvVar(envVars.CacheTTL);
|
||||
await cacheClient.set(key, JSON.stringify(response), { expires: ttl });
|
||||
logger.debug(`✅ INFO: Cached response for ${description} at "${url}" for ${ttl} seconds. (key: "${key}")`);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function fetchWithLogging<T>(url: string, description: string, options?: Options): Promise<T | null> {
|
||||
try {
|
||||
const config: AxiosRequestConfig = options || {};
|
||||
const response = await axios.get<T>(url, config);
|
||||
|
|
|
@ -5,6 +5,7 @@ const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_';
|
|||
const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_';
|
||||
const CORS_PREFIX = PREFIX + 'CORS_';
|
||||
const LOGGING_PREFIX = PREFIX + 'LOGGING_';
|
||||
const CACHE_PREFIX = PREFIX + 'CACHE_';
|
||||
|
||||
interface EnvVar {
|
||||
key: string;
|
||||
|
@ -39,6 +40,11 @@ export const envVars: Record<string, EnvVar> = {
|
|||
|
||||
LogLevel: { key: LOGGING_PREFIX + 'LEVEL', defaultValue: 'info' },
|
||||
LokiHost: { key: LOGGING_PREFIX + 'LOKI_HOST', defaultValue: 'http://localhost:3102' },
|
||||
|
||||
CacheHost: { key: CACHE_PREFIX + 'HOST' },
|
||||
CachePort: { key: CACHE_PREFIX + 'PORT', defaultValue: 11211 },
|
||||
CacheTTL: { key: CACHE_PREFIX + 'TTL', defaultValue: 60 * 60 * 24 }, // 24 hours
|
||||
CacheKeyPrefix: { key: CACHE_PREFIX + 'KEY_PREFIX', defaultValue: 'dwengo' },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
@ -56,7 +62,7 @@ export function getEnvVar(envVar: EnvVar): string {
|
|||
} else if (envVar.required) {
|
||||
throw new Error(`Missing environment variable: ${envVar.key}`);
|
||||
} else {
|
||||
return String(envVar.defaultValue) || '';
|
||||
return envVar.defaultValue !== undefined ? String(envVar.defaultValue) || '' : '';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import { Language } from '@dwengo-1/common/util/language';
|
|||
import { getAllAnswersHandler, getAnswerHandler, updateAnswerHandler } from '../../src/controllers/answers';
|
||||
import { BadRequestException } from '../../src/exceptions/bad-request-exception';
|
||||
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||
import { getQuestion02 } from '../test_assets/questions/questions.testdata';
|
||||
import { getAnswer02 } from '../test_assets/questions/answers.testdata';
|
||||
|
||||
describe('Questions controllers', () => {
|
||||
|
|
76
backend/tests/controllers/assignments.test.ts
Normal file
76
backend/tests/controllers/assignments.test.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { setupTestApp } from '../setup-tests.js';
|
||||
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||
import { Request, Response } from 'express';
|
||||
import { getAssignmentHandler, getAllAssignmentsHandler, getAssignmentsSubmissionsHandler } from '../../src/controllers/assignments.js';
|
||||
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||
import { getClass01 } from '../test_assets/classes/classes.testdata';
|
||||
import { getAssignment01 } from '../test_assets/assignments/assignments.testdata';
|
||||
|
||||
function createRequestObject(
|
||||
classid: string,
|
||||
assignmentid: string
|
||||
): {
|
||||
query: { full: string };
|
||||
params: { classid: string; id: string };
|
||||
} {
|
||||
return {
|
||||
params: {
|
||||
classid: classid,
|
||||
id: assignmentid,
|
||||
},
|
||||
query: {
|
||||
full: 'true',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Assignment controllers', () => {
|
||||
let req: Partial<Request>;
|
||||
let res: Partial<Response>;
|
||||
|
||||
let jsonMock: Mock;
|
||||
let statusMock: Mock;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestApp();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jsonMock = vi.fn();
|
||||
statusMock = vi.fn().mockReturnThis();
|
||||
|
||||
res = {
|
||||
json: jsonMock,
|
||||
status: statusMock,
|
||||
};
|
||||
});
|
||||
|
||||
it('return error non-existing assignment', async () => {
|
||||
req = createRequestObject('doesnotexist', '43000'); // Should not exist
|
||||
|
||||
await expect(async () => getAssignmentHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should return an assignment', async () => {
|
||||
const assignment = getAssignment01();
|
||||
req = createRequestObject(assignment.within.classId as string, (assignment.id ?? 1).toString());
|
||||
|
||||
await getAssignmentHandler(req as Request, res as Response);
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ assignment: expect.anything() }));
|
||||
});
|
||||
|
||||
it('should return a list of assignments', async () => {
|
||||
req = createRequestObject(getClass01().classId as string, 'irrelevant');
|
||||
|
||||
await getAllAssignmentsHandler(req as Request, res as Response);
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ assignments: expect.anything() }));
|
||||
});
|
||||
|
||||
it('should return a list of submissions for an assignment', async () => {
|
||||
const assignment = getAssignment01();
|
||||
req = createRequestObject(assignment.within.classId as string, (assignment.id ?? 1).toString());
|
||||
|
||||
await getAssignmentsSubmissionsHandler(req as Request, res as Response);
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
|
||||
});
|
||||
});
|
123
backend/tests/controllers/classes.test.ts
Normal file
123
backend/tests/controllers/classes.test.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { setupTestApp } from '../setup-tests.js';
|
||||
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||
import {
|
||||
createClassHandler,
|
||||
deleteClassHandler,
|
||||
getAllClassesHandler,
|
||||
getClassHandler,
|
||||
getClassStudentsHandler,
|
||||
getTeacherInvitationsHandler,
|
||||
} from '../../src/controllers/classes.js';
|
||||
import { Request, Response } from 'express';
|
||||
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||
import { BadRequestException } from '../../src/exceptions/bad-request-exception';
|
||||
import { getClass01 } from '../test_assets/classes/classes.testdata';
|
||||
describe('Class controllers', () => {
|
||||
let req: Partial<Request>;
|
||||
let res: Partial<Response>;
|
||||
|
||||
let jsonMock: Mock;
|
||||
let statusMock: Mock;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestApp();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jsonMock = vi.fn();
|
||||
statusMock = vi.fn().mockReturnThis();
|
||||
|
||||
res = {
|
||||
json: jsonMock,
|
||||
status: statusMock,
|
||||
};
|
||||
});
|
||||
|
||||
it('create and delete class', async () => {
|
||||
req = {
|
||||
body: { displayName: 'coole_nieuwe_klas' },
|
||||
};
|
||||
|
||||
await createClassHandler(req as Request, res as Response);
|
||||
|
||||
const result = jsonMock.mock.lastCall?.[0];
|
||||
// Console.log('class', result.class);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ class: expect.anything() }));
|
||||
|
||||
req = {
|
||||
params: { id: result.class.id },
|
||||
};
|
||||
|
||||
await deleteClassHandler(req as Request, res as Response);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ class: expect.anything() }));
|
||||
});
|
||||
|
||||
it('Error class not found', async () => {
|
||||
req = {
|
||||
params: { id: 'doesnotexist' },
|
||||
};
|
||||
|
||||
await expect(async () => getClassHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('Error create a class without name', async () => {
|
||||
req = {
|
||||
body: {},
|
||||
};
|
||||
|
||||
await expect(async () => createClassHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('return list of students', async () => {
|
||||
req = {
|
||||
params: { id: getClass01().classId as string },
|
||||
query: {},
|
||||
};
|
||||
|
||||
await getClassStudentsHandler(req as Request, res as Response);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ students: expect.anything() }));
|
||||
});
|
||||
|
||||
it('Error students on a non-existent class', async () => {
|
||||
req = {
|
||||
params: { id: 'doesnotexist' },
|
||||
query: {},
|
||||
};
|
||||
|
||||
await expect(async () => getClassStudentsHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should return 200 and a list of teacher-invitations', async () => {
|
||||
const classId = getClass01().classId as string;
|
||||
req = {
|
||||
params: { id: classId },
|
||||
query: {},
|
||||
};
|
||||
|
||||
await getTeacherInvitationsHandler(req as Request, res as Response);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() }));
|
||||
});
|
||||
|
||||
it('Error teacher-invitations on a non-existent class', async () => {
|
||||
req = {
|
||||
params: { id: 'doesnotexist' },
|
||||
query: {},
|
||||
};
|
||||
|
||||
await expect(async () => getTeacherInvitationsHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should return a list of classes', async () => {
|
||||
req = {
|
||||
query: {},
|
||||
};
|
||||
|
||||
await getAllClassesHandler(req as Request, res as Response);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ classes: expect.anything() }));
|
||||
});
|
||||
});
|
140
backend/tests/controllers/groups.test.ts
Normal file
140
backend/tests/controllers/groups.test.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
import { setupTestApp } from '../setup-tests.js';
|
||||
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||
import { Request, Response } from 'express';
|
||||
import {
|
||||
createGroupHandler,
|
||||
deleteGroupHandler,
|
||||
getAllGroupsHandler,
|
||||
getGroupHandler,
|
||||
getGroupSubmissionsHandler,
|
||||
} from '../../src/controllers/groups.js';
|
||||
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||
import { getClass01 } from '../test_assets/classes/classes.testdata';
|
||||
import { getAssignment01, getAssignment02 } from '../test_assets/assignments/assignments.testdata';
|
||||
import { getTestGroup01 } from '../test_assets/assignments/groups.testdata';
|
||||
|
||||
function createRequestObject(
|
||||
classid: string,
|
||||
assignmentid: string,
|
||||
groupNumber: string
|
||||
): {
|
||||
query: { full: string };
|
||||
params: { classid: string; groupid: string; assignmentid: string };
|
||||
} {
|
||||
return {
|
||||
params: {
|
||||
classid: classid,
|
||||
assignmentid: assignmentid,
|
||||
groupid: groupNumber,
|
||||
},
|
||||
query: {
|
||||
full: 'true',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Group controllers', () => {
|
||||
let req: Partial<Request>;
|
||||
let res: Partial<Response>;
|
||||
|
||||
let jsonMock: Mock;
|
||||
let statusMock: Mock;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestApp();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jsonMock = vi.fn();
|
||||
statusMock = vi.fn().mockReturnThis();
|
||||
|
||||
res = {
|
||||
json: jsonMock,
|
||||
status: statusMock,
|
||||
};
|
||||
});
|
||||
|
||||
it('Error not found on a non-existing group', async () => {
|
||||
req = {
|
||||
params: {
|
||||
classid: 'id01',
|
||||
assignmentid: '1',
|
||||
groupid: '154981', // Should not exist
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should return 404 not found on a non-existing assignment', async () => {
|
||||
req = {
|
||||
params: {
|
||||
classid: 'id01',
|
||||
assignmentid: '1000', // Should not exist
|
||||
groupid: '42000', // Should not exist
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should return 404 not found ont a non-existing class', async () => {
|
||||
req = {
|
||||
params: {
|
||||
classid: 'doesnotexist', // Should not exist
|
||||
assignmentid: '1000', // Should not exist
|
||||
groupid: '42000', // Should not exist
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should return an existing group', async () => {
|
||||
const group = getTestGroup01();
|
||||
const classId = getClass01().classId as string;
|
||||
req = createRequestObject(classId, (group.assignment.id ?? 1).toString(), (group.groupNumber ?? 1).toString());
|
||||
|
||||
await getGroupHandler(req as Request, res as Response);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ group: expect.anything() }));
|
||||
});
|
||||
|
||||
it('Create and delete', async () => {
|
||||
const assignment = getAssignment02();
|
||||
const classId = assignment.within.classId as string;
|
||||
req = createRequestObject(classId, (assignment.id ?? 1).toString(), '1');
|
||||
req.body = {
|
||||
members: ['Noordkaap', 'DireStraits'],
|
||||
};
|
||||
|
||||
await createGroupHandler(req as Request, res as Response);
|
||||
|
||||
await deleteGroupHandler(req as Request, res as Response);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ group: expect.anything() }));
|
||||
});
|
||||
|
||||
it('should return the submissions for a group', async () => {
|
||||
const group = getTestGroup01();
|
||||
const classId = getClass01().classId as string;
|
||||
req = createRequestObject(classId, (group.assignment.id ?? 1).toString(), (group.groupNumber ?? 1).toString());
|
||||
|
||||
await getGroupSubmissionsHandler(req as Request, res as Response);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
|
||||
});
|
||||
|
||||
it('should return a list of groups for an assignment', async () => {
|
||||
const assignment = getAssignment01();
|
||||
const classId = assignment.within.classId as string;
|
||||
req = createRequestObject(classId, (assignment.id ?? 1).toString(), '1');
|
||||
|
||||
await getAllGroupsHandler(req as Request, res as Response);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ groups: expect.anything() }));
|
||||
});
|
||||
});
|
|
@ -15,7 +15,7 @@ import {
|
|||
deleteClassJoinRequestHandler,
|
||||
getStudentRequestHandler,
|
||||
} from '../../src/controllers/students.js';
|
||||
import { getDireStraits, getNoordkaap, getPinkFloyd, getTheDoors, TEST_STUDENTS } from '../test_assets/users/students.testdata.js';
|
||||
import { getDireStraits, getNoordkaap, getTheDoors, TEST_STUDENTS } from '../test_assets/users/students.testdata.js';
|
||||
import { NotFoundException } from '../../src/exceptions/not-found-exception.js';
|
||||
import { BadRequestException } from '../../src/exceptions/bad-request-exception.js';
|
||||
import { ConflictException } from '../../src/exceptions/conflict-exception.js';
|
||||
|
@ -25,7 +25,6 @@ import { getClass02 } from '../test_assets/classes/classes.testdata.js';
|
|||
import { getClassJoinRequest02 } from '../test_assets/classes/class-join-requests.testdata.js';
|
||||
import { getTestGroup01 } from '../test_assets/assignments/groups.testdata.js';
|
||||
import { getSubmission01 } from '../test_assets/assignments/submission.testdata.js';
|
||||
import { getQuestion } from '../../src/services/questions.js';
|
||||
import { getQuestion01 } from '../test_assets/questions/questions.testdata.js';
|
||||
|
||||
describe('Student controllers', () => {
|
||||
|
|
61
backend/tests/controllers/submissions.test.ts
Normal file
61
backend/tests/controllers/submissions.test.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { setupTestApp } from '../setup-tests.js';
|
||||
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||
import { getSubmissionHandler, getAllSubmissionsHandler } from '../../src/controllers/submissions.js';
|
||||
import { Request, Response } from 'express';
|
||||
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||
import { getClass02 } from '../test_assets/classes/classes.testdata';
|
||||
|
||||
function createRequestObject(
|
||||
hruid: string,
|
||||
submissionNumber: string
|
||||
): {
|
||||
query: { language: string; version: string };
|
||||
params: { hruid: string; id: string };
|
||||
} {
|
||||
return {
|
||||
params: {
|
||||
hruid: hruid,
|
||||
id: submissionNumber,
|
||||
},
|
||||
query: {
|
||||
language: 'en',
|
||||
version: '1',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Submission controllers', () => {
|
||||
let req: Partial<Request>;
|
||||
let res: Partial<Response>;
|
||||
|
||||
let jsonMock: Mock;
|
||||
let statusMock: Mock;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestApp();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jsonMock = vi.fn();
|
||||
statusMock = vi.fn().mockReturnThis();
|
||||
|
||||
res = {
|
||||
json: jsonMock,
|
||||
status: statusMock,
|
||||
};
|
||||
});
|
||||
|
||||
it('error submission is not found', async () => {
|
||||
req = createRequestObject('id01', '1000000');
|
||||
|
||||
await expect(async () => getSubmissionHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should return a list of submissions for a learning object', async () => {
|
||||
req = createRequestObject(getClass02().classId as string, 'irrelevant');
|
||||
|
||||
await getAllSubmissionsHandler(req as Request, res as Response);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
|
||||
});
|
||||
});
|
|
@ -14,7 +14,6 @@ import { BadRequestException } from '../../src/exceptions/bad-request-exception'
|
|||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||
import { getTeacherInvitation01 } from '../test_assets/classes/teacher-invitations.testdata.js';
|
||||
import { getLimpBizkit, getTestleerkracht1 } from '../test_assets/users/teachers.testdata.js';
|
||||
import { getTestGroup01 } from '../test_assets/assignments/groups.testdata.js';
|
||||
import { getClass02 } from '../test_assets/classes/classes.testdata.js';
|
||||
|
||||
describe('Teacher controllers', () => {
|
||||
|
|
|
@ -15,12 +15,10 @@ import {
|
|||
import { BadRequestException } from '../../src/exceptions/bad-request-exception.js';
|
||||
import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js';
|
||||
import { getStudentRequestsHandler } from '../../src/controllers/students.js';
|
||||
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
||||
import { getClassHandler } from '../../src/controllers/classes';
|
||||
import { getFooFighters, getTestleerkracht1 } from '../test_assets/users/teachers.testdata.js';
|
||||
import { getClass02 } from '../test_assets/classes/classes.testdata.js';
|
||||
import { getPinkFloyd, TEST_STUDENTS } from '../test_assets/users/students.testdata.js';
|
||||
import { getClassJoinRequest01, getClassJoinRequest02 } from '../test_assets/classes/class-join-requests.testdata.js';
|
||||
import { getClassJoinRequest01 } from '../test_assets/classes/class-join-requests.testdata.js';
|
||||
|
||||
describe('Teacher controllers', () => {
|
||||
let req: Partial<Request>;
|
||||
|
@ -102,7 +100,7 @@ describe('Teacher controllers', () => {
|
|||
});
|
||||
|
||||
it('Teacher list', async () => {
|
||||
req = { query: { full: 'true' } };
|
||||
req = { query: { full: 'false' } };
|
||||
|
||||
await getAllTeachersHandler(req as Request, res as Response);
|
||||
|
||||
|
@ -110,12 +108,10 @@ describe('Teacher controllers', () => {
|
|||
|
||||
const result = jsonMock.mock.lastCall?.[0];
|
||||
|
||||
const teacherUsernames = result.teachers.map((s: TeacherDTO) => s.username);
|
||||
|
||||
const teacher = getTestleerkracht1();
|
||||
expect(teacherUsernames).toContain(teacher.username);
|
||||
expect(result.teachers).toContain(teacher.username);
|
||||
|
||||
expect(result.teachers).toHaveLength(5);
|
||||
expect(result.teachers.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('Deleting non-existent teacher', async () => {
|
||||
|
@ -156,27 +152,6 @@ describe('Teacher controllers', () => {
|
|||
expect(result.students.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
It('Get teacher questions', async () => {
|
||||
req = {
|
||||
params: { username: 'FooFighters' },
|
||||
query: { full: 'true' },
|
||||
};
|
||||
|
||||
await getTeacherQuestionHandler(req as Request, res as Response);
|
||||
|
||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ questions: expect.anything() }));
|
||||
|
||||
const result = jsonMock.mock.lastCall?.[0];
|
||||
// console.log('[TEACHER QUESTIONS]', result.questions);
|
||||
expect(result.questions.length).toBeGreaterThan(0);
|
||||
|
||||
// TODO fix
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
it('Get join requests by class', async () => {
|
||||
const jr = getClassJoinRequest01();
|
||||
req = {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|||
import { setupTestApp } from '../../setup-tests';
|
||||
import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository';
|
||||
import { getAssignmentRepository } from '../../../src/data/repositories';
|
||||
import { getClass01, getClass02 } from '../../test_assets/classes/classes.testdata';
|
||||
import { getClass02 } from '../../test_assets/classes/classes.testdata';
|
||||
import { getAssignment02, getAssignment03 } from '../../test_assets/assignments/assignments.testdata';
|
||||
import { getTestleerkracht1 } from '../../test_assets/users/teachers.testdata';
|
||||
|
||||
|
@ -17,7 +17,7 @@ describe('AssignmentRepository', () => {
|
|||
it('should return the requested assignment', async () => {
|
||||
const class_ = getClass02();
|
||||
const usedAssignment = getAssignment02();
|
||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 21001);
|
||||
const assignment = await assignmentRepository.findByClassAndId(class_, 21001);
|
||||
|
||||
expect(assignment).toBeTruthy();
|
||||
expect(assignment!.description).toBe(usedAssignment.description);
|
||||
|
@ -30,7 +30,7 @@ describe('AssignmentRepository', () => {
|
|||
it('should return all assignments for a class', async () => {
|
||||
const class_ = getClass02();
|
||||
const usedAssignment = getAssignment02();
|
||||
const assignments = await assignmentRepository.findAllAssignmentsInClass(class_!);
|
||||
const assignments = await assignmentRepository.findAllAssignmentsInClass(class_);
|
||||
|
||||
expect(assignments).toBeTruthy();
|
||||
expect(assignments).toHaveLength(1);
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { setupTestApp } from '../../setup-tests';
|
||||
import { GroupRepository } from '../../../src/data/assignments/group-repository';
|
||||
import { getAssignmentRepository, getClassRepository, getGroupRepository } from '../../../src/data/repositories';
|
||||
import { getClass01 } from '../../test_assets/classes/classes.testdata';
|
||||
import { getGroupRepository } from '../../../src/data/repositories';
|
||||
import { getAssignment01, getAssignment02 } from '../../test_assets/assignments/assignments.testdata';
|
||||
import { getTestGroup01, getTestGroup02, getTestGroup03 } from '../../test_assets/assignments/groups.testdata';
|
||||
import { getDireStraits, getNoordkaap } from '../../test_assets/users/students.testdata';
|
||||
|
@ -21,7 +20,7 @@ describe('GroupRepository', () => {
|
|||
const member1 = getNoordkaap();
|
||||
const member2 = getDireStraits();
|
||||
|
||||
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, usedGroup.groupNumber!);
|
||||
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, usedGroup.groupNumber!);
|
||||
|
||||
expect(group).toBeTruthy();
|
||||
expect(group?.groupNumber).toBe(usedGroup.groupNumber);
|
||||
|
@ -36,7 +35,7 @@ describe('GroupRepository', () => {
|
|||
const gr2 = getTestGroup02();
|
||||
const gr3 = getTestGroup03();
|
||||
|
||||
const groups = await groupRepository.findAllGroupsForAssignment(assignment!);
|
||||
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
|
||||
|
||||
expect(groups).toBeTruthy();
|
||||
expect(groups).toHaveLength(3);
|
||||
|
@ -49,9 +48,9 @@ describe('GroupRepository', () => {
|
|||
const assignment = getAssignment02();
|
||||
const deleted = getTestGroup01();
|
||||
|
||||
await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, deleted.groupNumber!);
|
||||
await groupRepository.deleteByAssignmentAndGroupNumber(assignment, deleted.groupNumber!);
|
||||
|
||||
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, deleted.groupNumber!);
|
||||
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, deleted.groupNumber!);
|
||||
|
||||
expect(group).toBeNull();
|
||||
});
|
||||
|
|
|
@ -1,21 +1,11 @@
|
|||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { setupTestApp } from '../../setup-tests';
|
||||
import { SubmissionRepository } from '../../../src/data/assignments/submission-repository';
|
||||
import {
|
||||
getAssignmentRepository,
|
||||
getClassRepository,
|
||||
getGroupRepository,
|
||||
getStudentRepository,
|
||||
getSubmissionRepository,
|
||||
} from '../../../src/data/repositories';
|
||||
import { getSubmissionRepository } from '../../../src/data/repositories';
|
||||
import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { Submission } from '../../../src/entities/assignments/submission.entity';
|
||||
import { Class } from '../../../src/entities/classes/class.entity';
|
||||
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
|
||||
import { testLearningObject01, testLearningObject03 } from '../../test_assets/content/learning-objects.testdata';
|
||||
import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata';
|
||||
import { getSubmission01, getSubmission02, getSubmission07, getSubmission08 } from '../../test_assets/assignments/submission.testdata';
|
||||
import { use } from 'marked';
|
||||
import { getAssignment01 } from '../../test_assets/assignments/assignments.testdata';
|
||||
import { getTestGroup02 } from '../../test_assets/assignments/groups.testdata';
|
||||
|
||||
|
@ -29,7 +19,11 @@ describe('SubmissionRepository', () => {
|
|||
|
||||
it('should find the requested submission', async () => {
|
||||
const usedSubmission = getSubmission01();
|
||||
const id = new LearningObjectIdentifier(usedSubmission.learningObjectHruid, usedSubmission.learningObjectLanguage, usedSubmission.learningObjectVersion);
|
||||
const id = new LearningObjectIdentifier(
|
||||
usedSubmission.learningObjectHruid,
|
||||
usedSubmission.learningObjectLanguage,
|
||||
usedSubmission.learningObjectVersion
|
||||
);
|
||||
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, usedSubmission.submissionNumber!);
|
||||
|
||||
expect(submission).toBeTruthy();
|
||||
|
@ -40,9 +34,13 @@ describe('SubmissionRepository', () => {
|
|||
|
||||
it('should find the most recent submission for a student', async () => {
|
||||
const usedSubmission = getSubmission02();
|
||||
const id = new LearningObjectIdentifier(usedSubmission.learningObjectHruid, usedSubmission.learningObjectLanguage, usedSubmission.learningObjectVersion);
|
||||
|
||||
const submission = await submissionRepository.findMostRecentSubmissionForStudent(id, usedSubmission.submitter!);
|
||||
const id = new LearningObjectIdentifier(
|
||||
usedSubmission.learningObjectHruid,
|
||||
usedSubmission.learningObjectLanguage,
|
||||
usedSubmission.learningObjectVersion
|
||||
);
|
||||
|
||||
const submission = await submissionRepository.findMostRecentSubmissionForStudent(id, usedSubmission.submitter);
|
||||
|
||||
expect(submission).toBeTruthy();
|
||||
expect(submission?.submissionTime).toStrictEqual(usedSubmission.submissionTime);
|
||||
|
@ -50,8 +48,12 @@ describe('SubmissionRepository', () => {
|
|||
|
||||
it('should find the most recent submission for a group', async () => {
|
||||
const usedSubmission = getSubmission02();
|
||||
const id = new LearningObjectIdentifier(usedSubmission.learningObjectHruid, usedSubmission.learningObjectLanguage, usedSubmission.learningObjectVersion);
|
||||
|
||||
const id = new LearningObjectIdentifier(
|
||||
usedSubmission.learningObjectHruid,
|
||||
usedSubmission.learningObjectLanguage,
|
||||
usedSubmission.learningObjectVersion
|
||||
);
|
||||
|
||||
const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, usedSubmission.onBehalfOf);
|
||||
|
||||
expect(submission).toBeTruthy();
|
||||
|
@ -61,13 +63,13 @@ describe('SubmissionRepository', () => {
|
|||
it('should find all submissions for a certain learning object and assignment', async () => {
|
||||
const usedSubmission = getSubmission08();
|
||||
const assignment = getAssignment01();
|
||||
|
||||
|
||||
const loId = {
|
||||
hruid: usedSubmission.learningObjectHruid,
|
||||
language: usedSubmission.learningObjectLanguage,
|
||||
version: usedSubmission.learningObjectVersion,
|
||||
};
|
||||
const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!);
|
||||
const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment);
|
||||
sortSubmissions(result);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
|
@ -94,7 +96,7 @@ describe('SubmissionRepository', () => {
|
|||
version: usedSubmission.learningObjectVersion,
|
||||
};
|
||||
|
||||
const result = await submissionRepository.findAllSubmissionsForLearningObjectAndGroup(loId, group!);
|
||||
const result = await submissionRepository.findAllSubmissionsForLearningObjectAndGroup(loId, group);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { setupTestApp } from '../../setup-tests';
|
||||
import { ClassJoinRequestRepository } from '../../../src/data/classes/class-join-request-repository';
|
||||
import { getClassJoinRequestRepository, getClassRepository, getStudentRepository } from '../../../src/data/repositories';
|
||||
import { getClassJoinRequestRepository } from '../../../src/data/repositories';
|
||||
import { getPinkFloyd, getSmashingPumpkins } from '../../test_assets/users/students.testdata';
|
||||
import { getClass02, getClass03 } from '../../test_assets/classes/classes.testdata';
|
||||
import { getClassJoinRequest01, getClassJoinRequest02, getClassJoinRequest03 } from '../../test_assets/classes/class-join-requests.testdata';
|
||||
|
@ -30,7 +30,7 @@ describe('ClassJoinRequestRepository', () => {
|
|||
const class_ = getClass02();
|
||||
const jr1 = getClassJoinRequest01();
|
||||
const jr2 = getClassJoinRequest02();
|
||||
const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_!);
|
||||
const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_);
|
||||
|
||||
expect(requests).toBeTruthy();
|
||||
expect(requests).toHaveLength(2);
|
||||
|
@ -41,9 +41,9 @@ describe('ClassJoinRequestRepository', () => {
|
|||
it('should not find a removed request', async () => {
|
||||
const studentUsed = getSmashingPumpkins();
|
||||
const class_ = getClass03();
|
||||
await classJoinRequestRepository.deleteBy(studentUsed!, class_!);
|
||||
await classJoinRequestRepository.deleteBy(studentUsed, class_);
|
||||
|
||||
const request = await classJoinRequestRepository.findAllRequestsBy(studentUsed!);
|
||||
const request = await classJoinRequestRepository.findAllRequestsBy(studentUsed);
|
||||
|
||||
expect(request).toHaveLength(0);
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ClassRepository } from '../../../src/data/classes/class-repository';
|
|||
import { setupTestApp } from '../../setup-tests';
|
||||
import { getClassRepository } from '../../../src/data/repositories';
|
||||
import { getClass01, getClass04 } from '../../test_assets/classes/classes.testdata';
|
||||
import { getClass01, getClass04 } from '../../test_assets/classes/classes.testdata';
|
||||
|
||||
describe('ClassRepository', () => {
|
||||
let classRepository: ClassRepository;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { setupTestApp } from '../../setup-tests';
|
||||
import { getTeacherInvitationRepository, getTeacherRepository } from '../../../src/data/repositories';
|
||||
import { getTeacherInvitationRepository } from '../../../src/data/repositories';
|
||||
import { TeacherInvitationRepository } from '../../../src/data/classes/teacher-invitation-repository';
|
||||
import { getFooFighters, getLimpBizkit } from '../../test_assets/users/teachers.testdata';
|
||||
import { getTeacherInvitation01, getTeacherInvitation02, getTeacherInvitation03 } from '../../test_assets/classes/teacher-invitations.testdata';
|
||||
|
@ -18,7 +18,7 @@ describe('ClassRepository', () => {
|
|||
const teacher = getLimpBizkit();
|
||||
const ti1 = getTeacherInvitation01();
|
||||
const ti2 = getTeacherInvitation02();
|
||||
const invitations = await teacherInvitationRepository.findAllInvitationsBy(teacher!);
|
||||
const invitations = await teacherInvitationRepository.findAllInvitationsBy(teacher);
|
||||
|
||||
expect(invitations).toBeTruthy();
|
||||
expect(invitations).toHaveLength(2);
|
||||
|
@ -30,7 +30,7 @@ describe('ClassRepository', () => {
|
|||
const teacher = getFooFighters();
|
||||
const ti1 = getTeacherInvitation01();
|
||||
const ti2 = getTeacherInvitation03();
|
||||
const invitations = await teacherInvitationRepository.findAllInvitationsFor(teacher!);
|
||||
const invitations = await teacherInvitationRepository.findAllInvitationsFor(teacher);
|
||||
|
||||
expect(invitations).toBeTruthy();
|
||||
expect(invitations).toHaveLength(2);
|
||||
|
@ -42,22 +42,21 @@ describe('ClassRepository', () => {
|
|||
const class_ = getClass02();
|
||||
const ti1 = getTeacherInvitation01();
|
||||
const ti2 = getTeacherInvitation02();
|
||||
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_!);
|
||||
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_);
|
||||
|
||||
expect(invitations).toBeTruthy();
|
||||
expect(invitations).toHaveLength(2);
|
||||
expect(invitations[0].class.classId).toBeOneOf([ti1.class.classId, ti2.class.classId]);
|
||||
expect(invitations[1].class.classId).toBeOneOf([ti1.class.classId, ti2.class.classId]);
|
||||
|
||||
});
|
||||
|
||||
it('should not find a removed invitation', async () => {
|
||||
const class_ = getClass01();
|
||||
const sender = getFooFighters();
|
||||
const receiver = getLimpBizkit();
|
||||
await teacherInvitationRepository.deleteBy(class_!, sender!, receiver!);
|
||||
await teacherInvitationRepository.deleteBy(class_, sender, receiver);
|
||||
|
||||
const invitation = await teacherInvitationRepository.findAllInvitationsBy(sender!);
|
||||
const invitation = await teacherInvitationRepository.findAllInvitationsBy(sender);
|
||||
|
||||
expect(invitation).toHaveLength(0);
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|||
import { setupTestApp } from '../../setup-tests.js';
|
||||
import { getAttachmentRepository } from '../../../src/data/repositories.js';
|
||||
import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js';
|
||||
import { testLearningObject02 } from '../../test_assets/content/learning-objects.testdata';
|
||||
import { getAttachment01 } from '../../test_assets/content/attachments.testdata.js';
|
||||
|
||||
describe('AttachmentRepository', () => {
|
||||
|
|
|
@ -6,7 +6,6 @@ import { LearningObject } from '../../../src/entities/content/learning-object.en
|
|||
import { expectToBeCorrectEntity } from '../../test-utils/expectations.js';
|
||||
import { testLearningObject01, testLearningObject02, testLearningObject03 } from '../../test_assets/content/learning-objects.testdata';
|
||||
import { v4 } from 'uuid';
|
||||
import { wrap } from '@mikro-orm/core';
|
||||
|
||||
describe('LearningObjectRepository', () => {
|
||||
let learningObjectRepository: LearningObjectRepository;
|
||||
|
@ -38,7 +37,7 @@ describe('LearningObjectRepository', () => {
|
|||
let newerExample: LearningObject;
|
||||
|
||||
it('should allow a learning object with the same id except a different version to be added', async () => {
|
||||
// structeredClone failed on teacher, this copies all fields to a json object
|
||||
// StructeredClone failed on teacher, this copies all fields to a json object
|
||||
const testLearningObject01Newer = { ...testLearningObject01 };
|
||||
testLearningObject01Newer.version = 10;
|
||||
testLearningObject01Newer.title += ' (nieuw)';
|
||||
|
@ -49,12 +48,10 @@ describe('LearningObjectRepository', () => {
|
|||
});
|
||||
|
||||
it('should return the newest version of the learning object when queried by only hruid and language', async () => {
|
||||
|
||||
|
||||
const result = await learningObjectRepository.findLatestByHruidAndLanguage(newerExample.hruid, newerExample.language);
|
||||
// expect(result).toBeInstanceOf(LearningObject);
|
||||
// expect(result?.version).toBe(10);
|
||||
// expect(result?.title).toContain('(nieuw)');
|
||||
expect(result).toBeInstanceOf(LearningObject);
|
||||
expect(result?.version).toBe(10);
|
||||
expect(result?.title).toContain('(nieuw)');
|
||||
});
|
||||
|
||||
it('should return null when queried by non-existing hruid or language', async () => {
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { setupTestApp } from '../../setup-tests';
|
||||
import { AnswerRepository } from '../../../src/data/questions/answer-repository';
|
||||
import { getAnswerRepository, getQuestionRepository, getTeacherRepository } from '../../../src/data/repositories';
|
||||
import { QuestionRepository } from '../../../src/data/questions/question-repository';
|
||||
import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
import { TeacherRepository } from '../../../src/data/users/teacher-repository';
|
||||
import { getQuestion01, getQuestion02, getQuestion04, getQuestion05, getQuestion06 } from '../../test_assets/questions/questions.testdata';
|
||||
import { getAnswerRepository } from '../../../src/data/repositories';
|
||||
import { getQuestion01, getQuestion02 } from '../../test_assets/questions/questions.testdata';
|
||||
import { getAnswer01, getAnswer02, getAnswer03 } from '../../test_assets/questions/answers.testdata';
|
||||
import { getFooFighters } from '../../test_assets/users/teachers.testdata';
|
||||
import { testLearningObject05 } from '../../test_assets/content/learning-objects.testdata';
|
||||
|
||||
describe('AnswerRepository', () => {
|
||||
let answerRepository: AnswerRepository;
|
||||
|
@ -24,7 +19,7 @@ describe('AnswerRepository', () => {
|
|||
const a1 = getAnswer01();
|
||||
const a2 = getAnswer02();
|
||||
|
||||
const answers = await answerRepository.findAllAnswersToQuestion(question!);
|
||||
const answers = await answerRepository.findAllAnswersToQuestion(question);
|
||||
|
||||
expect(answers).toBeTruthy();
|
||||
expect(answers).toHaveLength(2);
|
||||
|
@ -38,7 +33,7 @@ describe('AnswerRepository', () => {
|
|||
|
||||
await answerRepository.createAnswer({
|
||||
toQuestion: question,
|
||||
author: teacher!,
|
||||
author: teacher,
|
||||
content: 'created answer',
|
||||
});
|
||||
|
||||
|
|
|
@ -48,8 +48,8 @@ describe('QuestionRepository', () => {
|
|||
const group = getTestGroup01();
|
||||
await questionRepository.createQuestion({
|
||||
loId: id,
|
||||
inGroup: group!,
|
||||
author: student!,
|
||||
inGroup: group,
|
||||
author: student,
|
||||
content: 'question?',
|
||||
});
|
||||
const question = await questionRepository.findAllQuestionsAboutLearningObject(id);
|
||||
|
@ -66,7 +66,7 @@ describe('QuestionRepository', () => {
|
|||
language: testLearningObject05.language,
|
||||
version: testLearningObject05.version,
|
||||
};
|
||||
const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!);
|
||||
const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment);
|
||||
sortQuestions(result);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
|
@ -94,7 +94,7 @@ describe('QuestionRepository', () => {
|
|||
};
|
||||
const assignment = getAssignment01();
|
||||
|
||||
const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment!, getTool().username);
|
||||
const result = await questionRepository.findAllQuestionsAboutLearningObjectInAssignment(loId, assignment, getTool().username);
|
||||
// (student Tool is in group #2)
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
|
|
|
@ -2,7 +2,6 @@ import { setupTestApp } from '../../setup-tests.js';
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { StudentRepository } from '../../../src/data/users/student-repository.js';
|
||||
import { getStudentRepository } from '../../../src/data/repositories.js';
|
||||
import { getNameOfJSDocTypedef } from 'typescript';
|
||||
import { getNoordkaap } from '../../test_assets/users/students.testdata.js';
|
||||
|
||||
const username = 'teststudent';
|
||||
|
|
|
@ -5,6 +5,12 @@ import { testLearningPath01, testLearningPath02, testLearningPathWithConditions
|
|||
import { getClass01, getClass02, getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata';
|
||||
|
||||
export function makeTestAssignemnts(em: EntityManager): Assignment[] {
|
||||
const futureDate = new Date();
|
||||
futureDate.setDate(futureDate.getDate() + 7);
|
||||
const pastDate = new Date();
|
||||
pastDate.setDate(pastDate.getDate() - 7);
|
||||
const today = new Date();
|
||||
today.setHours(23, 59);
|
||||
assignment01 = em.create(Assignment, {
|
||||
id: 21000,
|
||||
within: getClass01(),
|
||||
|
@ -12,6 +18,7 @@ export function makeTestAssignemnts(em: EntityManager): Assignment[] {
|
|||
description: 'reading',
|
||||
learningPathHruid: testLearningPath02.hruid,
|
||||
learningPathLanguage: testLearningPath02.language as Language,
|
||||
deadline: today,
|
||||
groups: [],
|
||||
});
|
||||
|
||||
|
@ -22,6 +29,7 @@ export function makeTestAssignemnts(em: EntityManager): Assignment[] {
|
|||
description: 'reading',
|
||||
learningPathHruid: testLearningPath01.hruid,
|
||||
learningPathLanguage: testLearningPath01.language as Language,
|
||||
deadline: futureDate,
|
||||
groups: [],
|
||||
});
|
||||
|
||||
|
@ -32,6 +40,7 @@ export function makeTestAssignemnts(em: EntityManager): Assignment[] {
|
|||
description: 'will be deleted',
|
||||
learningPathHruid: testLearningPath02.hruid,
|
||||
learningPathLanguage: testLearningPath02.language as Language,
|
||||
deadline: pastDate,
|
||||
groups: [],
|
||||
});
|
||||
|
||||
|
@ -42,6 +51,7 @@ export function makeTestAssignemnts(em: EntityManager): Assignment[] {
|
|||
description: 'with a description',
|
||||
learningPathHruid: testLearningPath01.hruid,
|
||||
learningPathLanguage: testLearningPath01.language as Language,
|
||||
deadline: pastDate,
|
||||
groups: [],
|
||||
});
|
||||
|
||||
|
@ -52,6 +62,7 @@ export function makeTestAssignemnts(em: EntityManager): Assignment[] {
|
|||
description: 'You have to do the testing learning path with a condition.',
|
||||
learningPathHruid: testLearningPathWithConditions.hruid,
|
||||
learningPathLanguage: testLearningPathWithConditions.language as Language,
|
||||
deadline: futureDate,
|
||||
groups: [],
|
||||
});
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ export function makeTestGroups(em: EntityManager): Group[] {
|
|||
* Group #1 for Assignment #1 in class 'id01'
|
||||
* => Assigned to do learning path 'id02'
|
||||
*/
|
||||
// gets deleted
|
||||
// Gets deleted
|
||||
group01 = em.create(Group, {
|
||||
assignment: getAssignment01(),
|
||||
groupNumber: 21001,
|
||||
|
|
|
@ -71,7 +71,7 @@ export function makeTestSubmissions(em: EntityManager): Submission[] {
|
|||
content: '',
|
||||
});
|
||||
|
||||
// gets deleted
|
||||
// Gets deleted
|
||||
submission07 = em.create(Submission, {
|
||||
learningObjectHruid: testLearningObject01.hruid,
|
||||
learningObjectLanguage: testLearningObject01.language,
|
||||
|
@ -106,34 +106,34 @@ let submission06: Submission;
|
|||
let submission07: Submission;
|
||||
let submission08: Submission;
|
||||
|
||||
export function getSubmission01(): Submission{
|
||||
export function getSubmission01(): Submission {
|
||||
return submission01;
|
||||
}
|
||||
|
||||
export function getSubmission02(): Submission{
|
||||
export function getSubmission02(): Submission {
|
||||
return submission02;
|
||||
}
|
||||
|
||||
export function getSubmission03(): Submission{
|
||||
export function getSubmission03(): Submission {
|
||||
return submission03;
|
||||
}
|
||||
|
||||
export function getSubmission04(): Submission{
|
||||
export function getSubmission04(): Submission {
|
||||
return submission04;
|
||||
}
|
||||
|
||||
export function getSubmission05(): Submission{
|
||||
export function getSubmission05(): Submission {
|
||||
return submission05;
|
||||
}
|
||||
|
||||
export function getSubmission06(): Submission{
|
||||
export function getSubmission06(): Submission {
|
||||
return submission06;
|
||||
}
|
||||
|
||||
export function getSubmission07(): Submission{
|
||||
export function getSubmission07(): Submission {
|
||||
return submission07;
|
||||
}
|
||||
|
||||
export function getSubmission08(): Submission{
|
||||
export function getSubmission08(): Submission {
|
||||
return submission08;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,18 +37,18 @@ let classJoinRequest02: ClassJoinRequest;
|
|||
let classJoinRequest03: ClassJoinRequest;
|
||||
let classJoinRequest04: ClassJoinRequest;
|
||||
|
||||
export function getClassJoinRequest01(): ClassJoinRequest{
|
||||
export function getClassJoinRequest01(): ClassJoinRequest {
|
||||
return classJoinRequest01;
|
||||
}
|
||||
|
||||
export function getClassJoinRequest02(): ClassJoinRequest{
|
||||
export function getClassJoinRequest02(): ClassJoinRequest {
|
||||
return classJoinRequest02;
|
||||
}
|
||||
|
||||
export function getClassJoinRequest03(): ClassJoinRequest{
|
||||
export function getClassJoinRequest03(): ClassJoinRequest {
|
||||
return classJoinRequest03;
|
||||
}
|
||||
|
||||
export function getClassJoinRequest04(): ClassJoinRequest{
|
||||
export function getClassJoinRequest04(): ClassJoinRequest {
|
||||
return classJoinRequest04;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export function makeTestClasses(em: EntityManager): Class[] {
|
|||
const teacherClass01: Teacher[] = [getTestleerkracht1()];
|
||||
|
||||
class01 = em.create(Class, {
|
||||
classId: '8764b861-90a6-42e5-9732-c0d9eb2f55f9',
|
||||
classId: 'X2J9QT', // 8764b861-90a6-42e5-9732-c0d9eb2f55f9
|
||||
displayName: 'class01',
|
||||
teachers: teacherClass01,
|
||||
students: studentsClass01,
|
||||
|
@ -20,7 +20,7 @@ export function makeTestClasses(em: EntityManager): Class[] {
|
|||
const teacherClass02: Teacher[] = [getLimpBizkit()];
|
||||
|
||||
class02 = em.create(Class, {
|
||||
classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89',
|
||||
classId: '7KLPMA', // 34d484a1-295f-4e9f-bfdc-3e7a23d86a89
|
||||
displayName: 'class02',
|
||||
teachers: teacherClass02,
|
||||
students: studentsClass02,
|
||||
|
@ -30,7 +30,7 @@ export function makeTestClasses(em: EntityManager): Class[] {
|
|||
const teacherClass03: Teacher[] = [getStaind()];
|
||||
|
||||
class03 = em.create(Class, {
|
||||
classId: '80dcc3e0-1811-4091-9361-42c0eee91cfa',
|
||||
classId: 'R0D3UZ', // 80dcc3e0-1811-4091-9361-42c0eee91cfa
|
||||
displayName: 'class03',
|
||||
teachers: teacherClass03,
|
||||
students: studentsClass03,
|
||||
|
@ -39,16 +39,16 @@ export function makeTestClasses(em: EntityManager): Class[] {
|
|||
const studentsClass04: Student[] = [getNoordkaap(), getDireStraits()];
|
||||
const teacherClass04: Teacher[] = [getStaind()];
|
||||
|
||||
// gets deleted in test
|
||||
// Gets deleted in test
|
||||
class04 = em.create(Class, {
|
||||
classId: '33d03536-83b8-4880-9982-9bbf2f908ddf',
|
||||
classId: 'Q8N5YC', // 33d03536-83b8-4880-9982-9bbf2f908ddf
|
||||
displayName: 'class04',
|
||||
teachers: teacherClass04,
|
||||
students: studentsClass04,
|
||||
});
|
||||
|
||||
classWithTestleerlingAndTestleerkracht = em.create(Class, {
|
||||
classId: 'a75298b5-18aa-471d-8eeb-5d77eb989393',
|
||||
classId: 'ZAV71B', // Was a75298b5-18aa-471d-8eeb-5d77eb989393
|
||||
displayName: 'Testklasse',
|
||||
teachers: [getTestleerkracht1()],
|
||||
students: [getTestleerling1()],
|
||||
|
|
|
@ -26,7 +26,7 @@ export function makeTestTeacherInvitations(em: EntityManager): TeacherInvitation
|
|||
status: ClassStatus.Open,
|
||||
});
|
||||
|
||||
// gets deleted in test
|
||||
// Gets deleted in test
|
||||
teacherInvitation04 = em.create(TeacherInvitation, {
|
||||
sender: getFooFighters(),
|
||||
receiver: getLimpBizkit(),
|
||||
|
@ -56,4 +56,4 @@ export function getTeacherInvitation03(): TeacherInvitation {
|
|||
|
||||
export function getTeacherInvitation04(): TeacherInvitation {
|
||||
return teacherInvitation04;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { testLearningObject01 } from './learning-objects.testdata';
|
|||
import { LearningObject } from '../../../src/entities/content/learning-object.entity';
|
||||
|
||||
export function makeTestAttachments(em: EntityManager): Attachment[] {
|
||||
// prevent duplicate insertion
|
||||
// Prevent duplicate insertion
|
||||
const lo = em.merge(LearningObject, testLearningObject01);
|
||||
|
||||
attachment01 = em.create(Attachment, {
|
||||
|
@ -19,6 +19,6 @@ export function makeTestAttachments(em: EntityManager): Attachment[] {
|
|||
|
||||
let attachment01: Attachment;
|
||||
|
||||
export function getAttachment01(): Attachment{
|
||||
export function getAttachment01(): Attachment {
|
||||
return attachment01;
|
||||
}
|
||||
|
|
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