feat(backend): PUSH, PUT en DELETE endpoints voor leerpaden aangemaakt.
This commit is contained in:
parent
20c04370b5
commit
30ca3b70de
8 changed files with 186 additions and 44 deletions
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[]>;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue