feat(backend): PUSH, PUT en DELETE endpoints voor leerpaden aangemaakt.

This commit is contained in:
Gerald Schmittinger 2025-05-12 16:11:08 +02:00
parent 20c04370b5
commit 30ca3b70de
8 changed files with 186 additions and 44 deletions

View file

@ -7,51 +7,89 @@ 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.js';
import { LearningPath, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import { getTeacher } from '../services/teachers.js';
/**
* Fetch learning paths based on query parameters.
*/
export async function getLearningPaths(req: Request, 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 learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup);
res.json(learningPaths.data);
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;
} else {
hruidList = themes.flatMap((theme) => theme.hruids);
}
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: LearningPath = req.body;
if (isPut) {
if (req.params.hruid !== path.hruid || req.params.language !== path.language) {
throw new BadRequestException("id_not_matching_query_params");
}
}
const teacher = await getTeacher(req.auth!.username);
res.json(await learningPathService.createNewLearningPath(path, [teacher], isPut));
}
}
export const postLearningPath = postOrPutLearningPath(false);
export const putLearningPath = postOrPutLearningPath(true);
export async function deleteLearningPath(req: AuthenticatedRequest, res: Response): Promise<void> {
const id: LearningPathIdentifier = {
hruid: req.params.hruid,
language: req.params.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.");
}
}

View file

@ -28,6 +28,21 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
});
}
/**
* 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: {
$contains: {
username: adminUsername
}
}
}
});
}
public createNode(nodeData: RequiredEntityData<LearningPathNode>): LearningPathNode {
return this.em.create(LearningPathNode, nodeData);
}
@ -50,4 +65,16 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
await Promise.all(nodes.map(async (it) => em.persistAndFlush(it)));
await Promise.all(transitions.map(async (it) => em.persistAndFlush(it)));
}
/**
* 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);
}
return path;
}
}

View file

@ -0,0 +1,12 @@
import learningPathService from "../../../services/learning-paths/learning-path-service";
import { authorize } from "../auth";
import { AuthenticatedRequest } from "../authenticated-request";
import { AuthenticationInfo } from "../authentication-info";
export const onlyAdminsForLearningPath = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => {
const adminsForLearningPath = learningPathService.getAdmins({
hruid: req.body.hruid,
language: req.body.language
});
return adminsForLearningPath && auth.username in adminsForLearningPath;
});

View file

@ -1,5 +1,7 @@
import express from 'express';
import { getLearningPaths } from '../controllers/learning-paths.js';
import { deleteLearningPath, getLearningPaths, postLearningPath, putLearningPath } from '../controllers/learning-paths.js';
import { teachersOnly } from '../middleware/auth/auth.js';
import { onlyAdminsForLearningObject } from '../middleware/auth/checks/learning-object-auth-checks.js';
const router = express.Router();
@ -23,5 +25,9 @@ const router = express.Router();
// Example: http://localhost:3000/learningPath?theme=kiks
router.get('/', getLearningPaths);
router.post('/', teachersOnly, postLearningPath)
router.put('/:hruid/:language', onlyAdminsForLearningObject, putLearningPath);
router.delete('/:hruid/:language', onlyAdminsForLearningObject, deleteLearningPath);
export default router;

View file

@ -198,6 +198,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.
*/

View file

@ -45,6 +45,10 @@ const dwengoApiLearningPathProvider: LearningPathProvider = {
const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params });
return searchResults ?? [];
},
async getLearningPathsAdministratedBy(_adminUsername: string) {
return []; // Learning paths fetched from the Dwengo API cannot be administrated by a user.
},
};
export default dwengoApiLearningPathProvider;

View file

@ -15,4 +15,9 @@ export interface LearningPathProvider {
* Search learning paths in the data source using the given search string.
*/
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[]>;
}

View file

@ -1,7 +1,7 @@
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 +12,7 @@ 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';
const userContentPrefix = getEnvVar(envVars.UserContentPrefix);
const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider];
@ -105,6 +106,16 @@ 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.
*/
@ -119,12 +130,42 @@ 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.
* @param allowReplace If this is set to true and there is already a learning path with the same identifier, it is replaced.
* @returns the created learning path.
*/
async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<void> {
async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[], allowReplace = false): Promise<LearningPathEntity> {
const repo = getLearningPathRepository();
const path = mapToLearningPath(dto, admins);
await repo.save(path, { preventOverwrite: true });
await repo.save(path, { preventOverwrite: allowReplace });
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);
}
};
export default learningPathService;