Merge branch 'feat/endpoints-in-backend-om-eigen-leerpaden-en-leerobjecten-toe-te-voegen-aan-de-databank-#248' of https://github.com/SELab-2/Dwengo-1 into feat/endpoints-in-backend-om-eigen-leerpaden-en-leerobjecten-toe-te-voegen-aan-de-databank-#248

This commit is contained in:
Gerald Schmittinger 2025-05-14 01:54:12 +02:00
commit 226c9786dd
32 changed files with 357 additions and 303 deletions

View file

@ -7,8 +7,8 @@ import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { NotFoundException } from '../exceptions/not-found-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js';
import { envVars, getEnvVar } from '../util/envVars.js'; import { envVars, getEnvVar } from '../util/envVars.js';
import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import {UploadedFile} from "express-fileupload"; import { UploadedFile } from 'express-fileupload';
import {AuthenticatedRequest} from "../middleware/auth/authenticated-request"; import { AuthenticatedRequest } from '../middleware/auth/authenticated-request';
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO { function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO {
if (!req.params.hruid) { if (!req.params.hruid) {
@ -32,12 +32,13 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif
} }
export async function getAllLearningObjects(req: Request, res: Response): Promise<void> { export async function getAllLearningObjects(req: Request, res: Response): Promise<void> {
if (req.query.admin) { // If the admin query parameter is present, the user wants to have all learning objects with this admin. if (req.query.admin) {
const learningObjects = // If the admin query parameter is present, the user wants to have all learning objects with this admin.
await learningObjectService.getLearningObjectsAdministratedBy(req.query.admin as string); const learningObjects = await learningObjectService.getLearningObjectsAdministratedBy(req.query.admin as string);
res.json(learningObjects); res.json(learningObjects);
} else { // Else he/she wants all learning objects on the path specified by the request parameters. } else {
// Else he/she wants all learning objects on the path specified by the request parameters.
const learningPathId = getLearningPathIdentifierFromRequest(req); const learningPathId = getLearningPathIdentifierFromRequest(req);
const full = req.query.full; const full = req.query.full;
@ -86,10 +87,9 @@ export async function handlePostLearningObject(req: AuthenticatedRequest, res: R
if (!req.files || !req.files.learningObject) { if (!req.files || !req.files.learningObject) {
throw new BadRequestException('No file uploaded'); throw new BadRequestException('No file uploaded');
} }
const learningObject = await learningObjectService.storeLearningObject( const learningObject = await learningObjectService.storeLearningObject((req.files.learningObject as UploadedFile).tempFilePath, [
(req.files.learningObject as UploadedFile).tempFilePath, req.auth!.username,
[req.auth!.username] ]);
);
res.json(learningObject); res.json(learningObject);
} }
@ -97,17 +97,17 @@ export async function handleDeleteLearningObject(req: AuthenticatedRequest, res:
const learningObjectId = getLearningObjectIdentifierFromRequest(req); const learningObjectId = getLearningObjectIdentifierFromRequest(req);
if (!learningObjectId.version) { if (!learningObjectId.version) {
throw new BadRequestException("When deleting a learning object, a version must be specified."); throw new BadRequestException('When deleting a learning object, a version must be specified.');
} }
const deletedLearningObject = await learningObjectService.deleteLearningObject({ const deletedLearningObject = await learningObjectService.deleteLearningObject({
hruid: learningObjectId.hruid, hruid: learningObjectId.hruid,
version: learningObjectId.version, version: learningObjectId.version,
language: learningObjectId.language language: learningObjectId.language,
}); });
if (deletedLearningObject) { if (deletedLearningObject) {
res.json(deletedLearningObject); res.json(deletedLearningObject);
} else { } else {
throw new NotFoundException("Learning object not found"); throw new NotFoundException('Learning object not found');
} }
} }

View file

@ -60,7 +60,12 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
hruidList = themes.flatMap((theme) => theme.hruids); hruidList = themes.flatMap((theme) => theme.hruids);
} }
const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup); const learningPaths = await learningPathService.fetchLearningPaths(
hruidList,
language as Language,
`HRUIDs: ${hruidList.join(', ')}`,
forGroup
);
res.json(learningPaths.data); res.json(learningPaths.data);
} }
} }
@ -71,12 +76,12 @@ function postOrPutLearningPath(isPut: boolean): (req: AuthenticatedRequest, res:
const teacher = await getTeacher(req.auth!.username); const teacher = await getTeacher(req.auth!.username);
if (isPut) { if (isPut) {
if (req.params.hruid !== path.hruid || req.params.language !== path.language) { if (req.params.hruid !== path.hruid || req.params.language !== path.language) {
throw new BadRequestException("id_not_matching_query_params"); throw new BadRequestException('id_not_matching_query_params');
} }
await learningPathService.deleteLearningPath({hruid: path.hruid, language: path.language as Language}); await learningPathService.deleteLearningPath({ hruid: path.hruid, language: path.language as Language });
} }
res.json(await learningPathService.createNewLearningPath(path, [teacher])); res.json(await learningPathService.createNewLearningPath(path, [teacher]));
} };
} }
export const postLearningPath = postOrPutLearningPath(false); export const postLearningPath = postOrPutLearningPath(false);
@ -85,12 +90,12 @@ export const putLearningPath = postOrPutLearningPath(true);
export async function deleteLearningPath(req: AuthenticatedRequest, res: Response): Promise<void> { export async function deleteLearningPath(req: AuthenticatedRequest, res: Response): Promise<void> {
const id: LearningPathIdentifier = { const id: LearningPathIdentifier = {
hruid: req.params.hruid, hruid: req.params.hruid,
language: req.params.language as Language language: req.params.language as Language,
}; };
const deletedPath = await learningPathService.deleteLearningPath(id); const deletedPath = await learningPathService.deleteLearningPath(id);
if (deletedPath) { if (deletedPath) {
res.json(deletedPath); res.json(deletedPath);
} else { } else {
throw new NotFoundException("The learning path could not be found."); throw new NotFoundException('The learning path could not be found.');
} }
} }

View file

@ -36,8 +36,8 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
return this.find( return this.find(
{ {
admins: { admins: {
username: adminUsername username: adminUsername,
} },
}, },
{ populate: ['admins'] } // Make sure to load admin relations { populate: ['admins'] } // Make sure to load admin relations
); );
@ -50,5 +50,4 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
} }
return learningObject; return learningObject;
} }
} }

View file

@ -7,10 +7,7 @@ import { LearningPathTransition } from '../../entities/content/learning-path-tra
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
return this.findOne( return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions', 'admins'] });
{ hruid: hruid, language: language },
{ populate: ['nodes', 'nodes.transitions', 'admins'] }
);
} }
/** /**
@ -37,10 +34,10 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
return this.findAll({ return this.findAll({
where: { where: {
admins: { admins: {
username: adminUsername username: adminUsername,
} },
}, },
populate: ['nodes', 'nodes.transitions', 'admins'] populate: ['nodes', 'nodes.transitions', 'admins'],
}); });
} }

View file

@ -9,7 +9,7 @@ export class Attachment {
@ManyToOne({ @ManyToOne({
entity: () => LearningObject, entity: () => LearningObject,
primary: true, primary: true,
deleteRule: 'cascade' deleteRule: 'cascade',
}) })
learningObject!: LearningObject; learningObject!: LearningObject;

View file

@ -1,14 +1,4 @@
import { import { ArrayType, Collection, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
ArrayType,
Collection,
Embedded,
Entity,
Enum,
ManyToMany,
OneToMany,
PrimaryKey,
Property
} from '@mikro-orm/core';
import { Attachment } from './attachment.entity.js'; import { Attachment } from './attachment.entity.js';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
@ -92,7 +82,7 @@ export class LearningObject {
@OneToMany({ @OneToMany({
entity: () => Attachment, entity: () => Attachment,
mappedBy: 'learningObject' mappedBy: 'learningObject',
}) })
attachments: Collection<Attachment> = new Collection<Attachment>(this); attachments: Collection<Attachment> = new Collection<Attachment>(this);

View file

@ -116,14 +116,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
* to true. * to true.
*/ */
export function authorize( export function authorize(accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise<boolean>): RequestHandler {
accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise<boolean> return async (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): Promise<void> => {
): RequestHandler {
return async (
req: AuthenticatedRequest,
_res: express.Response,
next: express.NextFunction
): Promise<void> => {
if (!req.auth) { if (!req.auth) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} else if (!(await accessCondition(req.auth, req))) { } else if (!(await accessCondition(req.auth, req))) {

View file

@ -1,8 +1,8 @@
import { Language } from "@dwengo-1/common/util/language"; import { Language } from '@dwengo-1/common/util/language';
import learningObjectService from "../../../services/learning-objects/learning-object-service"; import learningObjectService from '../../../services/learning-objects/learning-object-service';
import { authorize } from "../auth"; import { authorize } from '../auth';
import { AuthenticatedRequest } from "../authenticated-request"; import { AuthenticatedRequest } from '../authenticated-request';
import { AuthenticationInfo } from "../authentication-info"; import { AuthenticationInfo } from '../authentication-info';
export const onlyAdminsForLearningObject = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { export const onlyAdminsForLearningObject = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
const { hruid } = req.params; const { hruid } = req.params;
@ -10,7 +10,7 @@ export const onlyAdminsForLearningObject = authorize(async (auth: Authentication
const admins = await learningObjectService.getAdmins({ const admins = await learningObjectService.getAdmins({
hruid, hruid,
language: language as Language, language: language as Language,
version: parseInt(version as string) version: parseInt(version as string),
}); });
return admins.includes(auth.username); return admins.includes(auth.username);
}); });

View file

@ -1,13 +1,13 @@
import { Language } from "@dwengo-1/common/util/language"; import { Language } from '@dwengo-1/common/util/language';
import learningPathService from "../../../services/learning-paths/learning-path-service"; import learningPathService from '../../../services/learning-paths/learning-path-service';
import { authorize } from "../auth"; import { authorize } from '../auth';
import { AuthenticatedRequest } from "../authenticated-request"; import { AuthenticatedRequest } from '../authenticated-request';
import { AuthenticationInfo } from "../authentication-info"; import { AuthenticationInfo } from '../authentication-info';
export const onlyAdminsForLearningPath = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { export const onlyAdminsForLearningPath = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
const adminsForLearningPath = await learningPathService.getAdmins({ const adminsForLearningPath = await learningPathService.getAdmins({
hruid: req.params.hruid, hruid: req.params.hruid,
language: req.params.language as Language language: req.params.language as Language,
}); });
return adminsForLearningPath && adminsForLearningPath.includes(auth.username); return adminsForLearningPath && adminsForLearningPath.includes(auth.username);
}); });

View file

@ -5,12 +5,12 @@ import {
getLearningObject, getLearningObject,
getLearningObjectHTML, getLearningObjectHTML,
handleDeleteLearningObject, handleDeleteLearningObject,
handlePostLearningObject handlePostLearningObject,
} from '../controllers/learning-objects.js'; } from '../controllers/learning-objects.js';
import submissionRoutes from './submissions.js'; import submissionRoutes from './submissions.js';
import questionRoutes from './questions.js'; import questionRoutes from './questions.js';
import fileUpload from "express-fileupload"; import fileUpload from 'express-fileupload';
import { teachersOnly } from '../middleware/auth/auth.js'; import { teachersOnly } from '../middleware/auth/auth.js';
import { onlyAdminsForLearningObject } from '../middleware/auth/checks/learning-object-auth-checks.js'; import { onlyAdminsForLearningObject } from '../middleware/auth/checks/learning-object-auth-checks.js';
@ -28,7 +28,7 @@ const router = express.Router();
// Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie // Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie
router.get('/', getAllLearningObjects); router.get('/', getAllLearningObjects);
router.post('/', teachersOnly, fileUpload({useTempFiles: true}), handlePostLearningObject) router.post('/', teachersOnly, fileUpload({ useTempFiles: true }), handlePostLearningObject);
// Parameter: hruid of learning object // Parameter: hruid of learning object
// Query: language // Query: language
@ -40,7 +40,7 @@ router.get('/:hruid', getLearningObject);
// Query: language // Query: language
// Route to delete a learning object based on its hruid. // Route to delete a learning object based on its hruid.
// Example: http://localhost:3000/learningObject/un_ai7?language=nl&version=1 // Example: http://localhost:3000/learningObject/un_ai7?language=nl&version=1
router.delete('/:hruid', onlyAdminsForLearningObject, handleDeleteLearningObject) router.delete('/:hruid', onlyAdminsForLearningObject, handleDeleteLearningObject);
router.use('/:hruid/submissions', submissionRoutes); router.use('/:hruid/submissions', submissionRoutes);

View file

@ -25,7 +25,7 @@ const router = express.Router();
// Example: http://localhost:3000/learningPath?theme=kiks // Example: http://localhost:3000/learningPath?theme=kiks
router.get('/', getLearningPaths); router.get('/', getLearningPaths);
router.post('/', teachersOnly, postLearningPath) router.post('/', teachersOnly, postLearningPath);
router.put('/:hruid/:language', onlyAdminsForLearningPath, putLearningPath); router.put('/:hruid/:language', onlyAdminsForLearningPath, putLearningPath);
router.delete('/:hruid/:language', onlyAdminsForLearningPath, deleteLearningPath); router.delete('/:hruid/:language', onlyAdminsForLearningPath, deleteLearningPath);

View file

@ -141,7 +141,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
*/ */
async getLearningObjectsAdministratedBy(_adminUsername: string): Promise<FilteredLearningObject[]> { async getLearningObjectsAdministratedBy(_adminUsername: string): Promise<FilteredLearningObject[]> {
return []; // The dwengo database does not contain any learning objects administrated by users. return []; // The dwengo database does not contain any learning objects administrated by users.
} },
}; };
export default dwengoApiLearningObjectProvider; export default dwengoApiLearningObjectProvider;

View file

@ -2,14 +2,10 @@ import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provid
import { LearningObjectProvider } from './learning-object-provider.js'; import { LearningObjectProvider } from './learning-object-provider.js';
import { envVars, getEnvVar } from '../../util/envVars.js'; import { envVars, getEnvVar } from '../../util/envVars.js';
import databaseLearningObjectProvider from './database-learning-object-provider.js'; import databaseLearningObjectProvider from './database-learning-object-provider.js';
import { import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
FilteredLearningObject, import { getLearningObjectRepository, getTeacherRepository } from '../../data/repositories';
LearningObjectIdentifierDTO, import { processLearningObjectZip } from './learning-object-zip-processing-service';
LearningPathIdentifier import { LearningObject } from '../../entities/content/learning-object.entity';
} from '@dwengo-1/common/interfaces/learning-content';
import {getLearningObjectRepository, getTeacherRepository} from "../../data/repositories";
import {processLearningObjectZip} from "./learning-object-zip-processing-service";
import {LearningObject} from "../../entities/content/learning-object.entity";
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { NotFoundException } from '../../exceptions/not-found-exception.js'; import { NotFoundException } from '../../exceptions/not-found-exception.js';
@ -74,17 +70,15 @@ const learningObjectService = {
// Lookup the admin teachers based on their usernames and add them to the admins of the learning object. // Lookup the admin teachers based on their usernames and add them to the admins of the learning object.
const teacherRepo = getTeacherRepository(); const teacherRepo = getTeacherRepository();
const adminTeachers = await Promise.all( const adminTeachers = await Promise.all(admins.map(async (it) => teacherRepo.findByUsername(it)));
admins.map(async it => teacherRepo.findByUsername(it)) adminTeachers.forEach((it) => {
);
adminTeachers.forEach(it => {
if (it !== null) { if (it !== null) {
learningObject.admins.add(it); learningObject.admins.add(it);
} }
}); });
try { try {
await learningObjectRepository.save(learningObject, {preventOverwrite: true}); await learningObjectRepository.save(learningObject, { preventOverwrite: true });
} catch (e: unknown) { } catch (e: unknown) {
learningObjectRepository.getEntityManager().clear(); learningObjectRepository.getEntityManager().clear();
throw e; throw e;
@ -109,10 +103,10 @@ const learningObjectService = {
const learningObjectRepo = getLearningObjectRepository(); const learningObjectRepo = getLearningObjectRepository();
const learningObject = await learningObjectRepo.findByIdentifier(id); const learningObject = await learningObjectRepo.findByIdentifier(id);
if (!learningObject) { if (!learningObject) {
throw new NotFoundException("The specified learning object does not exist."); throw new NotFoundException('The specified learning object does not exist.');
} }
return learningObject.admins.map(admin => admin.username); return learningObject.admins.map((admin) => admin.username);
} },
}; };
export default learningObjectService; export default learningObjectService;

View file

@ -1,9 +1,9 @@
import unzipper from 'unzipper'; import unzipper from 'unzipper';
import mime from 'mime-types'; import mime from 'mime-types';
import {LearningObject} from "../../entities/content/learning-object.entity"; import { LearningObject } from '../../entities/content/learning-object.entity';
import {getAttachmentRepository, getLearningObjectRepository} from "../../data/repositories"; import { getAttachmentRepository, getLearningObjectRepository } from '../../data/repositories';
import {BadRequestException} from "../../exceptions/bad-request-exception"; import { BadRequestException } from '../../exceptions/bad-request-exception';
import {LearningObjectMetadata} from "@dwengo-1/common/interfaces/learning-content"; import { LearningObjectMetadata } from '@dwengo-1/common/interfaces/learning-content';
import { DwengoContentType } from './processing/content-type'; import { DwengoContentType } from './processing/content-type';
const METADATA_PATH_REGEX = /.*[/^]metadata\.json$/; const METADATA_PATH_REGEX = /.*[/^]metadata\.json$/;
@ -17,22 +17,21 @@ export async function processLearningObjectZip(filePath: string): Promise<Learni
let zip: unzipper.CentralDirectory; let zip: unzipper.CentralDirectory;
try { try {
zip = await unzipper.Open.file(filePath); zip = await unzipper.Open.file(filePath);
} catch(_: unknown) { } catch (_: unknown) {
throw new BadRequestException("invalidZip"); throw new BadRequestException('invalidZip');
} }
let metadata: LearningObjectMetadata | undefined = undefined; let metadata: LearningObjectMetadata | undefined = undefined;
const attachments: {name: string, content: Buffer}[] = []; const attachments: { name: string; content: Buffer }[] = [];
let content: Buffer | undefined = undefined; let content: Buffer | undefined = undefined;
if (zip.files.length === 0) { if (zip.files.length === 0) {
throw new BadRequestException("emptyZip") throw new BadRequestException('emptyZip');
} }
await Promise.all( await Promise.all(
zip.files.map(async file => { zip.files.map(async (file) => {
if (file.type !== "Directory") { if (file.type !== 'Directory') {
if (METADATA_PATH_REGEX.test(file.path)) { if (METADATA_PATH_REGEX.test(file.path)) {
metadata = await processMetadataJson(file); metadata = await processMetadataJson(file);
} else if (CONTENT_PATH_REGEX.test(file.path)) { } else if (CONTENT_PATH_REGEX.test(file.path)) {
@ -40,7 +39,7 @@ export async function processLearningObjectZip(filePath: string): Promise<Learni
} else { } else {
attachments.push({ attachments.push({
name: file.path, name: file.path,
content: await processFile(file) content: await processFile(file),
}); });
} }
} }
@ -48,27 +47,24 @@ export async function processLearningObjectZip(filePath: string): Promise<Learni
); );
if (!metadata) { if (!metadata) {
throw new BadRequestException("missingMetadata"); throw new BadRequestException('missingMetadata');
} }
if (!content) { if (!content) {
throw new BadRequestException("missingIndex"); throw new BadRequestException('missingIndex');
} }
const learningObject = createLearningObject(metadata, content, attachments); const learningObject = createLearningObject(metadata, content, attachments);
return learningObject; return learningObject;
} }
function createLearningObject( function createLearningObject(metadata: LearningObjectMetadata, content: Buffer, attachments: { name: string; content: Buffer }[]): LearningObject {
metadata: LearningObjectMetadata, content: Buffer, attachments: { name: string; content: Buffer; }[]
): LearningObject {
const learningObjectRepo = getLearningObjectRepository(); const learningObjectRepo = getLearningObjectRepository();
const attachmentRepo = getAttachmentRepository(); const attachmentRepo = getAttachmentRepository();
const returnValue = { const returnValue = {
callbackUrl: metadata.return_value?.callback_url ?? "", callbackUrl: metadata.return_value?.callback_url ?? '',
callbackSchema: metadata.return_value?.callback_schema ? JSON.stringify(metadata.return_value.callback_schema) : "" callbackSchema: metadata.return_value?.callback_schema ? JSON.stringify(metadata.return_value.callback_schema) : '',
}; };
const learningObject = learningObjectRepo.create({ const learningObject = learningObjectRepo.create({
@ -76,26 +72,30 @@ function createLearningObject(
available: metadata.available ?? true, available: metadata.available ?? true,
content: content, content: content,
contentType: metadata.content_type as DwengoContentType, contentType: metadata.content_type as DwengoContentType,
copyright: metadata.copyright ?? "", copyright: metadata.copyright ?? '',
description: metadata.description ?? "", description: metadata.description ?? '',
educationalGoals: metadata.educational_goals ?? [], educationalGoals: metadata.educational_goals ?? [],
hruid: metadata.hruid, hruid: metadata.hruid,
keywords: metadata.keywords, keywords: metadata.keywords,
language: metadata.language, language: metadata.language,
license: metadata.license ?? "", license: metadata.license ?? '',
returnValue, returnValue,
skosConcepts: metadata.skos_concepts ?? [], skosConcepts: metadata.skos_concepts ?? [],
teacherExclusive: metadata.teacher_exclusive, teacherExclusive: metadata.teacher_exclusive,
title: metadata.title, title: metadata.title,
version: metadata.version version: metadata.version,
});
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);
}); });
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; return learningObject;
} }

View file

@ -112,12 +112,13 @@ async function convertNode(
) )
.map((trans, i) => { .map((trans, i) => {
try { try {
return convertTransition(trans, i, nodesToLearningObjects) return convertTransition(trans, i, nodesToLearningObjects);
} catch (_: unknown) { } catch (_: unknown) {
logger.error(`Transition could not be resolved: ${JSON.stringify(trans)}`); 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. return undefined; // Do not crash on invalid transitions, just ignore them so the rest of the learning path keeps working.
} }
}).filter(it => it !== undefined); })
.filter((it) => it !== undefined);
return { return {
_id: learningObject.uuid, _id: learningObject.uuid,
language: learningObject.language, language: learningObject.language,

View file

@ -110,9 +110,7 @@ const learningPathService = {
* Fetch the learning paths administrated by the teacher with the given username. * Fetch the learning paths administrated by the teacher with the given username.
*/ */
async getLearningPathsAdministratedBy(adminUsername: string): Promise<LearningPath[]> { async getLearningPathsAdministratedBy(adminUsername: string): Promise<LearningPath[]> {
const providerResponses = await Promise.all( const providerResponses = await Promise.all(allProviders.map(async (provider) => provider.getLearningPathsAdministratedBy(adminUsername)));
allProviders.map(async (provider) => provider.getLearningPathsAdministratedBy(adminUsername))
);
return providerResponses.flat(); return providerResponses.flat();
}, },
@ -157,7 +155,7 @@ const learningPathService = {
if (deletedPath) { if (deletedPath) {
return deletedPath; return deletedPath;
} }
throw new NotFoundException("No learning path with the given identifier found."); throw new NotFoundException('No learning path with the given identifier found.');
}, },
/** /**
@ -168,10 +166,10 @@ const learningPathService = {
const repo = getLearningPathRepository(); const repo = getLearningPathRepository();
const path = await repo.findByHruidAndLanguage(id.hruid, id.language); const path = await repo.findByHruidAndLanguage(id.hruid, id.language);
if (!path) { if (!path) {
throw new NotFoundException("No learning path with the given identifier found."); throw new NotFoundException('No learning path with the given identifier found.');
} }
return path.admins.map(admin => admin.username); return path.admins.map((admin) => admin.username);
} },
}; };
export default learningPathService; export default learningPathService;

View file

@ -5,7 +5,7 @@
import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts"; import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts";
import { useThemeQuery } from "@/queries/themes.ts"; import { useThemeQuery } from "@/queries/themes.ts";
import type { Theme } from "@/data-objects/theme.ts"; import type { Theme } from "@/data-objects/theme.ts";
import authService from "@/services/auth/auth-service"; import authService from "@/services/auth/auth-service";
const props = defineProps({ const props = defineProps({
selectedTheme: { type: String, required: true }, selectedTheme: { type: String, required: true },

View file

@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useI18n } from "vue-i18n";
const props = defineProps<{ const props = defineProps<{
text: string, text: string;
prependIcon?: string, prependIcon?: string;
appendIcon?: string, appendIcon?: string;
confirmQueryText: string, confirmQueryText: string;
variant?: "flat" | "text" | "elevated" | "tonal" | "outlined" | "plain" | undefined, variant?: "flat" | "text" | "elevated" | "tonal" | "outlined" | "plain" | undefined;
color?: string, color?: string;
disabled?: boolean disabled?: boolean;
}>(); }>();
const emit = defineEmits<{ (e: 'confirm'): void }>() const emit = defineEmits<{ (e: "confirm"): void }>();
const { t } = useI18n(); const { t } = useI18n();
@ -22,40 +22,42 @@
<template> <template>
<v-dialog max-width="500"> <v-dialog max-width="500">
<template v-slot:activator="{ props: activatorProps }"> <template v-slot:activator="{ props: activatorProps }">
<v-btn <v-btn
v-bind="activatorProps" v-bind="activatorProps"
:text="props.text" :text="props.text"
:prependIcon="props.prependIcon" :prependIcon="props.prependIcon"
:appendIcon="props.appendIcon" :appendIcon="props.appendIcon"
:variant="props.variant" :variant="props.variant"
:color="color" :color="color"
:disabled="props.disabled" :disabled="props.disabled"
></v-btn> ></v-btn>
</template> </template>
<template v-slot:default="{ isActive }"> <template v-slot:default="{ isActive }">
<v-card :title="t('confirmDialogTitle')"> <v-card :title="t('confirmDialogTitle')">
<v-card-text> <v-card-text>
{{ props.confirmQueryText }} {{ props.confirmQueryText }}
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn
:text="t('yes')" :text="t('yes')"
@click="confirm(); isActive.value = false" @click="
></v-btn> confirm();
<v-btn isActive.value = false;
:text="t('cancel')" "
@click="isActive.value = false" ></v-btn>
></v-btn> <v-btn
</v-card-actions> :text="t('cancel')"
</v-card> @click="isActive.value = false"
</template> ></v-btn>
</v-dialog> </v-card-actions>
</v-card>
</template>
</v-dialog>
</template> </template>
<style scoped> <style scoped></style>
</style>

View file

@ -7,7 +7,7 @@
// Import assets // Import assets
import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg";
import { useLocale } from "vuetify"; import { useLocale } from "vuetify";
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const { current: vuetifyLocale } = useLocale(); const { current: vuetifyLocale } = useLocale();

View file

@ -46,16 +46,21 @@ export abstract class BaseController {
* @param queryParams The query parameters. * @param queryParams The query parameters.
* @returns The response the POST request generated. * @returns The response the POST request generated.
*/ */
protected async postFile<T>(path: string, formFieldName: string, file: File, queryParams?: QueryParams): Promise<T> { protected async postFile<T>(
path: string,
formFieldName: string,
file: File,
queryParams?: QueryParams,
): Promise<T> {
const formData = new FormData(); const formData = new FormData();
formData.append(formFieldName, file); formData.append(formFieldName, file);
const response = await apiClient.post<T>(this.absolutePathFor(path), formData, { const response = await apiClient.post<T>(this.absolutePathFor(path), formData, {
params: queryParams, params: queryParams,
headers: { headers: {
'Content-Type': 'multipart/form-data' "Content-Type": "multipart/form-data",
} },
}); });
BaseController.assertSuccessResponse(response) BaseController.assertSuccessResponse(response);
return response.data; return response.data;
} }

View file

@ -22,7 +22,7 @@ export class LearningPathNode {
this.learningobjectHruid = options.learningobjectHruid; this.learningobjectHruid = options.learningobjectHruid;
this.version = options.version; this.version = options.version;
this.language = options.language; this.language = options.language;
this.transitions = options.transitions.map(it => ({ next: it.next, default: it.default ?? false })); this.transitions = options.transitions.map((it) => ({ next: it.next, default: it.default ?? false }));
this.createdAt = options.createdAt; this.createdAt = options.createdAt;
this.updatedAt = options.updatedAt; this.updatedAt = options.updatedAt;
this.done = options.done || false; this.done = options.done || false;

View file

@ -82,7 +82,7 @@ export class LearningPath {
keywords: dto.keywords.split(" "), keywords: dto.keywords.split(" "),
targetAges: { targetAges: {
min: dto.min_age ?? NaN, min: dto.min_age ?? NaN,
max: dto.max_age ?? NaN max: dto.max_age ?? NaN,
}, },
startNode: LearningPathNode.fromDTOAndOtherNodes(LearningPath.getStartNode(dto), dto.nodes), startNode: LearningPathNode.fromDTOAndOtherNodes(LearningPath.getStartNode(dto), dto.nodes),
image: dto.image, image: dto.image,

View file

@ -8,7 +8,7 @@ import * as directives from "vuetify/directives";
import i18n from "./i18n/i18n.ts"; import i18n from "./i18n/i18n.ts";
// JSON-editor // JSON-editor
import JsonEditorVue from 'json-editor-vue'; import JsonEditorVue from "json-editor-vue";
// Components // Components
import App from "./App.vue"; import App from "./App.vue";
@ -20,7 +20,7 @@ import { de, en, fr, nl } from "vuetify/locale";
const app = createApp(App); const app = createApp(App);
app.use(router); app.use(router);
app.use(JsonEditorVue, {}) app.use(JsonEditorVue, {});
const link = document.createElement("link"); const link = document.createElement("link");
link.rel = "stylesheet"; link.rel = "stylesheet";
@ -39,9 +39,9 @@ const vuetify = createVuetify({
}, },
locale: { locale: {
locale: i18n.global.locale, locale: i18n.global.locale,
fallback: 'en', fallback: "en",
messages: { nl, en, de, fr } messages: { nl, en, de, fr },
} },
}); });
const queryClient = new QueryClient({ const queryClient = new QueryClient({

View file

@ -1,6 +1,12 @@
import { type MaybeRefOrGetter, toValue } from "vue"; import { type MaybeRefOrGetter, toValue } from "vue";
import type { Language } from "@/data-objects/language.ts"; import type { Language } from "@/data-objects/language.ts";
import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import {
useMutation,
useQuery,
useQueryClient,
type UseMutationReturnType,
type UseQueryReturnType,
} from "@tanstack/vue-query";
import { getLearningObjectController } from "@/controllers/controllers.ts"; import { getLearningObjectController } from "@/controllers/controllers.ts";
import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts";
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
@ -58,7 +64,7 @@ export function useLearningObjectListForPathQuery(
} }
export function useLearningObjectListForAdminQuery( export function useLearningObjectListForAdminQuery(
admin: MaybeRefOrGetter<string | undefined> admin: MaybeRefOrGetter<string | undefined>,
): UseQueryReturnType<LearningObject[], Error> { ): UseQueryReturnType<LearningObject[], Error> {
return useQuery({ return useQuery({
queryKey: [LEARNING_OBJECT_KEY, "forAdmin", admin], queryKey: [LEARNING_OBJECT_KEY, "forAdmin", admin],
@ -66,24 +72,39 @@ export function useLearningObjectListForAdminQuery(
const adminVal = toValue(admin); const adminVal = toValue(admin);
return await learningObjectController.getAllAdministratedBy(adminVal!); return await learningObjectController.getAllAdministratedBy(adminVal!);
}, },
enabled: () => toValue(admin) !== undefined enabled: () => toValue(admin) !== undefined,
}); });
} }
export function useUploadLearningObjectMutation(): UseMutationReturnType<LearningObject, AxiosError, {learningObjectZip: File}, unknown> { export function useUploadLearningObjectMutation(): UseMutationReturnType<
LearningObject,
AxiosError,
{ learningObjectZip: File },
unknown
> {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ learningObjectZip }) => await learningObjectController.upload(learningObjectZip), mutationFn: async ({ learningObjectZip }) => await learningObjectController.upload(learningObjectZip),
onSuccess: async () => { await queryClient.invalidateQueries({queryKey: [LEARNING_OBJECT_KEY, "forAdmin"]}); } onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: [LEARNING_OBJECT_KEY, "forAdmin"] });
},
}); });
} }
export function useDeleteLearningObjectMutation(): UseMutationReturnType<LearningObject, AxiosError, {hruid: string, language: Language, version: number}, unknown> { export function useDeleteLearningObjectMutation(): UseMutationReturnType<
LearningObject,
AxiosError,
{ hruid: string; language: Language; version: number },
unknown
> {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ hruid, language, version }) => await learningObjectController.deleteLearningObject(hruid, language, version), mutationFn: async ({ hruid, language, version }) =>
onSuccess: async () => { await queryClient.invalidateQueries({queryKey: [LEARNING_OBJECT_KEY, "forAdmin"]}); } await learningObjectController.deleteLearningObject(hruid, language, version),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: [LEARNING_OBJECT_KEY, "forAdmin"] });
},
}); });
} }

View file

@ -1,6 +1,12 @@
import { type MaybeRefOrGetter, toValue } from "vue"; import { type MaybeRefOrGetter, toValue } from "vue";
import type { Language } from "@/data-objects/language.ts"; import type { Language } from "@/data-objects/language.ts";
import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import {
useMutation,
useQuery,
useQueryClient,
type UseMutationReturnType,
type UseQueryReturnType,
} from "@tanstack/vue-query";
import { getLearningPathController } from "@/controllers/controllers"; import { getLearningPathController } from "@/controllers/controllers";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content"; import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
@ -35,42 +41,54 @@ export function useGetAllLearningPathsByThemeQuery(
} }
export function useGetAllLearningPathsByAdminQuery( export function useGetAllLearningPathsByAdminQuery(
admin: MaybeRefOrGetter<string | undefined> admin: MaybeRefOrGetter<string | undefined>,
): UseQueryReturnType<LearningPathDTO[], AxiosError> { ): UseQueryReturnType<LearningPathDTO[], AxiosError> {
return useQuery({ return useQuery({
queryKey: [LEARNING_PATH_KEY, "getAllByAdmin", admin], queryKey: [LEARNING_PATH_KEY, "getAllByAdmin", admin],
queryFn: async () => learningPathController.getAllByAdminRaw(toValue(admin)!), queryFn: async () => learningPathController.getAllByAdminRaw(toValue(admin)!),
enabled: () => Boolean(toValue(admin)) enabled: () => Boolean(toValue(admin)),
}); });
} }
export function usePostLearningPathMutation(): export function usePostLearningPathMutation(): UseMutationReturnType<
UseMutationReturnType<LearningPathDTO, AxiosError, { learningPath: LearningPathDTO }, unknown> { LearningPathDTO,
AxiosError,
{ learningPath: LearningPathDTO },
unknown
> {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ learningPath }) => learningPathController.postLearningPath(learningPath), mutationFn: async ({ learningPath }) => learningPathController.postLearningPath(learningPath),
onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }) onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }),
}); });
} }
export function usePutLearningPathMutation(): export function usePutLearningPathMutation(): UseMutationReturnType<
UseMutationReturnType<LearningPathDTO, AxiosError, { learningPath: LearningPathDTO }, unknown> { LearningPathDTO,
AxiosError,
{ learningPath: LearningPathDTO },
unknown
> {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ learningPath }) => learningPathController.putLearningPath(learningPath), mutationFn: async ({ learningPath }) => learningPathController.putLearningPath(learningPath),
onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }) onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }),
}); });
} }
export function useDeleteLearningPathMutation(): export function useDeleteLearningPathMutation(): UseMutationReturnType<
UseMutationReturnType<LearningPathDTO, AxiosError, { hruid: string, language: Language }, unknown> { LearningPathDTO,
AxiosError,
{ hruid: string; language: Language },
unknown
> {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ hruid, language }) => learningPathController.deleteLearningPath(hruid, language), mutationFn: async ({ hruid, language }) => learningPathController.deleteLearningPath(hruid, language),
onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }) onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }),
}); });
} }

View file

@ -110,7 +110,7 @@ const router = createRouter({
path: "/my-content", path: "/my-content",
name: "OwnLearningContentPage", name: "OwnLearningContentPage",
component: OwnLearningContentPage, component: OwnLearningContentPage,
meta: { requiresAuth: true } meta: { requiresAuth: true },
}, },
{ {
path: "/learningPath", path: "/learningPath",
@ -126,7 +126,7 @@ const router = createRouter({
name: "LearningPath", name: "LearningPath",
component: LearningPathPage, component: LearningPathPage,
props: true, props: true,
meta: { requiresAuth: true } meta: { requiresAuth: true },
}, },
], ],
}, },

View file

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {useLearningObjectListForAdminQuery} from "@/queries/learning-objects.ts"; import { useLearningObjectListForAdminQuery } from "@/queries/learning-objects.ts";
import OwnLearningObjectsView from "@/views/own-learning-content/learning-objects/OwnLearningObjectsView.vue" import OwnLearningObjectsView from "@/views/own-learning-content/learning-objects/OwnLearningObjectsView.vue";
import OwnLearningPathsView from "@/views/own-learning-content/learning-paths/OwnLearningPathsView.vue" import OwnLearningPathsView from "@/views/own-learning-content/learning-paths/OwnLearningPathsView.vue";
import authService from "@/services/auth/auth-service.ts"; import authService from "@/services/auth/auth-service.ts";
import UsingQueryResult from "@/components/UsingQueryResult.vue"; import UsingQueryResult from "@/components/UsingQueryResult.vue";
import type { LearningObject } from "@/data-objects/learning-objects/learning-object"; import type { LearningObject } from "@/data-objects/learning-objects/learning-object";
@ -12,11 +12,13 @@
const { t } = useI18n(); const { t } = useI18n();
const learningObjectsQuery = const learningObjectsQuery = useLearningObjectListForAdminQuery(
useLearningObjectListForAdminQuery(authService.authState.user?.profile.preferred_username); authService.authState.user?.profile.preferred_username,
);
const learningPathsQuery = const learningPathsQuery = useGetAllLearningPathsByAdminQuery(
useGetAllLearningPathsByAdminQuery(authService.authState.user?.profile.preferred_username); authService.authState.user?.profile.preferred_username,
);
type Tab = "learningObjects" | "learningPaths"; type Tab = "learningObjects" | "learningPaths";
const tab: Ref<Tab> = ref("learningObjects"); const tab: Ref<Tab> = ref("learningObjects");
@ -27,12 +29,18 @@
<h1 class="title">{{ t("ownLearningContentTitle") }}</h1> <h1 class="title">{{ t("ownLearningContentTitle") }}</h1>
<v-tabs v-model="tab"> <v-tabs v-model="tab">
<v-tab value="learningObjects">{{ t('learningObjects') }}</v-tab> <v-tab value="learningObjects">{{ t("learningObjects") }}</v-tab>
<v-tab value="learningPaths">{{ t('learningPaths') }}</v-tab> <v-tab value="learningPaths">{{ t("learningPaths") }}</v-tab>
</v-tabs> </v-tabs>
<v-tabs-window v-model="tab" class="main-content"> <v-tabs-window
<v-tabs-window-item value="learningObjects" class="main-content"> v-model="tab"
class="main-content"
>
<v-tabs-window-item
value="learningObjects"
class="main-content"
>
<using-query-result <using-query-result
:query-result="learningObjectsQuery" :query-result="learningObjectsQuery"
v-slot="response: { data: LearningObject[] }" v-slot="response: { data: LearningObject[] }"
@ -45,7 +53,7 @@
:query-result="learningPathsQuery" :query-result="learningPathsQuery"
v-slot="response: { data: LearningPathDTO[] }" v-slot="response: { data: LearningPathDTO[] }"
> >
<own-learning-paths-view :learningPaths="response.data"/> <own-learning-paths-view :learningPaths="response.data" />
</using-query-result> </using-query-result>
</v-tabs-window-item> </v-tabs-window-item>
</v-tabs-window> </v-tabs-window>

View file

@ -1,21 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import type { LearningObject } from '@/data-objects/learning-objects/learning-object'; import type { LearningObject } from "@/data-objects/learning-objects/learning-object";
import UsingQueryResult from '@/components/UsingQueryResult.vue'; import UsingQueryResult from "@/components/UsingQueryResult.vue";
import LearningObjectContentView from '../../learning-paths/learning-object/content/LearningObjectContentView.vue'; import LearningObjectContentView from "../../learning-paths/learning-object/content/LearningObjectContentView.vue";
import ButtonWithConfirmation from '@/components/ButtonWithConfirmation.vue'; import ButtonWithConfirmation from "@/components/ButtonWithConfirmation.vue";
import { useDeleteLearningObjectMutation, useLearningObjectHTMLQuery } from '@/queries/learning-objects'; import { useDeleteLearningObjectMutation, useLearningObjectHTMLQuery } from "@/queries/learning-objects";
import { useI18n } from 'vue-i18n'; import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
selectedLearningObject?: LearningObject selectedLearningObject?: LearningObject;
}>(); }>();
const learningObjectQueryResult = useLearningObjectHTMLQuery( const learningObjectQueryResult = useLearningObjectHTMLQuery(
() => props.selectedLearningObject?.key, () => props.selectedLearningObject?.key,
() => props.selectedLearningObject?.language, () => props.selectedLearningObject?.language,
() => props.selectedLearningObject?.version () => props.selectedLearningObject?.version,
); );
const { isPending, mutate } = useDeleteLearningObjectMutation(); const { isPending, mutate } = useDeleteLearningObjectMutation();
@ -25,7 +25,7 @@
mutate({ mutate({
hruid: props.selectedLearningObject.key, hruid: props.selectedLearningObject.key,
language: props.selectedLearningObject.language, language: props.selectedLearningObject.language,
version: props.selectedLearningObject.version version: props.selectedLearningObject.version,
}); });
} }
} }
@ -37,7 +37,10 @@
:title="t('previewFor') + selectedLearningObject.title" :title="t('previewFor') + selectedLearningObject.title"
> >
<template v-slot:text> <template v-slot:text>
<using-query-result :query-result="learningObjectQueryResult" v-slot="response: { data: Document }"> <using-query-result
:query-result="learningObjectQueryResult"
v-slot="response: { data: Document }"
>
<learning-object-content-view :learning-object-content="response.data"></learning-object-content-view> <learning-object-content-view :learning-object-content="response.data"></learning-object-content-view>
</using-query-result> </using-query-result>
</template> </template>
@ -53,5 +56,4 @@
</v-card> </v-card>
</template> </template>
<style scoped> <style scoped></style>
</style>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useUploadLearningObjectMutation } from '@/queries/learning-objects'; import { useUploadLearningObjectMutation } from "@/queries/learning-objects";
import { ref, watch, type Ref } from 'vue'; import { ref, watch, type Ref } from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from "vue-i18n";
import { VFileUpload } from 'vuetify/labs/VFileUpload'; import { VFileUpload } from "vuetify/labs/VFileUpload";
const { t } = useI18n(); const { t } = useI18n();
@ -25,19 +25,23 @@
function uploadFile() { function uploadFile() {
if (fileToUpload.value) { if (fileToUpload.value) {
mutate({learningObjectZip: fileToUpload.value}); mutate({ learningObjectZip: fileToUpload.value });
} }
} }
</script> </script>
<template> <template>
<v-dialog max-width="500" v-model="dialogOpen"> <v-dialog
max-width="500"
v-model="dialogOpen"
>
<template v-slot:activator="{ props: activatorProps }"> <template v-slot:activator="{ props: activatorProps }">
<v-btn <v-btn
prepend-icon="mdi mdi-plus" prepend-icon="mdi mdi-plus"
:text="t('newLearningObject')" :text="t('newLearningObject')"
v-bind="activatorProps" v-bind="activatorProps"
color="rgb(14, 105, 66)" color="rgb(14, 105, 66)"
size="large"> size="large"
>
</v-btn> </v-btn>
</template> </template>
@ -75,5 +79,4 @@
</template> </template>
</v-dialog> </v-dialog>
</template> </template>
<style scoped> <style scoped></style>
</style>

View file

@ -1,36 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import type { LearningObject } from '@/data-objects/learning-objects/learning-object'; import type { LearningObject } from "@/data-objects/learning-objects/learning-object";
import LearningObjectUploadButton from '@/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue' import LearningObjectUploadButton from "@/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue";
import LearningObjectPreviewCard from './LearningObjectPreviewCard.vue'; import LearningObjectPreviewCard from "./LearningObjectPreviewCard.vue";
import { computed, ref, watch, type Ref } from 'vue'; import { computed, ref, watch, type Ref } from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
learningObjects: LearningObject[] learningObjects: LearningObject[];
}>(); }>();
const tableHeaders = [ const tableHeaders = [
{ title: t("hruid"), width: "250px", key: "key" }, { title: t("hruid"), width: "250px", key: "key" },
{ title: t("language"), width: "50px", key: "language" }, { title: t("language"), width: "50px", key: "language" },
{ title: t("version"), width: "50px", key: "version" }, { title: t("version"), width: "50px", key: "version" },
{ title: t("title"), key: "title" } { title: t("title"), key: "title" },
]; ];
const selectedLearningObjects: Ref<LearningObject[]> = ref([]); const selectedLearningObjects: Ref<LearningObject[]> = ref([]);
watch(() => props.learningObjects, () => selectedLearningObjects.value = []); watch(
() => props.learningObjects,
const selectedLearningObject = computed(() => () => (selectedLearningObjects.value = []),
selectedLearningObjects.value ? selectedLearningObjects.value[0] : undefined
); );
const selectedLearningObject = computed(() =>
selectedLearningObjects.value ? selectedLearningObjects.value[0] : undefined,
);
</script> </script>
<template> <template>
<div class="root"> <div class="root">
<div class="table-container"> <div class="table-container">
<learning-object-upload-button/> <learning-object-upload-button />
<v-data-table <v-data-table
class="table" class="table"
v-model="selectedLearningObjects" v-model="selectedLearningObjects"
@ -41,8 +43,14 @@
return-object return-object
/> />
</div> </div>
<div class="preview-container" v-if="selectedLearningObject"> <div
<learning-object-preview-card class="preview" :selectedLearningObject="selectedLearningObject"/> class="preview-container"
v-if="selectedLearningObject"
>
<learning-object-preview-card
class="preview"
:selectedLearningObject="selectedLearningObject"
/>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,30 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useI18n } from "vue-i18n";
import { computed, ref, watch, type Ref } from 'vue'; import { computed, ref, watch, type Ref } from "vue";
import JsonEditorVue from 'json-editor-vue' import JsonEditorVue from "json-editor-vue";
import ButtonWithConfirmation from '@/components/ButtonWithConfirmation.vue' import ButtonWithConfirmation from "@/components/ButtonWithConfirmation.vue";
import { useDeleteLearningPathMutation, usePostLearningPathMutation, usePutLearningPathMutation } from '@/queries/learning-paths'; import {
import { Language } from '@/data-objects/language'; useDeleteLearningPathMutation,
import type { LearningPath } from '@dwengo-1/common/interfaces/learning-content'; usePostLearningPathMutation,
import type { AxiosError } from 'axios'; usePutLearningPathMutation,
import { parse } from 'uuid'; } from "@/queries/learning-paths";
import { Language } from "@/data-objects/language";
import type { LearningPath } from "@dwengo-1/common/interfaces/learning-content";
import type { AxiosError } from "axios";
import { parse } from "uuid";
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
selectedLearningPath?: LearningPath selectedLearningPath?: LearningPath;
}>(); }>();
const { isPending, mutate, error: deleteError, isSuccess: deleteSuccess } = useDeleteLearningPathMutation(); const { isPending, mutate, error: deleteError, isSuccess: deleteSuccess } = useDeleteLearningPathMutation();
const DEFAULT_LEARNING_PATH: LearningPath = { const DEFAULT_LEARNING_PATH: LearningPath = {
language: 'en', language: "en",
hruid: '...', hruid: "...",
title: '...', title: "...",
description: '...', description: "...",
nodes: [ nodes: [
{ {
learningobject_hruid: '...', learningobject_hruid: "...",
language: Language.English, language: Language.English,
version: 1, version: 1,
start_node: true, start_node: true,
@ -33,17 +37,17 @@ import { parse } from 'uuid';
default: true, default: true,
condition: "(remove if the transition should be unconditinal)", condition: "(remove if the transition should be unconditinal)",
next: { next: {
hruid: '...', hruid: "...",
version: 1, version: 1,
language: '...' language: "...",
} },
} },
] ],
} },
], ],
keywords: 'Keywords separated by spaces', keywords: "Keywords separated by spaces",
target_ages: [] target_ages: [],
} };
const { isPending: isPostPending, error: postError, mutate: doPost } = usePostLearningPathMutation(); const { isPending: isPostPending, error: postError, mutate: doPost } = usePostLearningPathMutation();
const { isPending: isPutPending, error: putError, mutate: doPut } = usePutLearningPathMutation(); const { isPending: isPutPending, error: putError, mutate: doPut } = usePutLearningPathMutation();
@ -51,11 +55,13 @@ import { parse } from 'uuid';
const learningPath: Ref<LearningPath | string> = ref(DEFAULT_LEARNING_PATH); const learningPath: Ref<LearningPath | string> = ref(DEFAULT_LEARNING_PATH);
const parsedLearningPath = computed(() => const parsedLearningPath = computed(() =>
typeof learningPath.value === "string" ? JSON.parse(learningPath.value) as LearningPath typeof learningPath.value === "string" ? (JSON.parse(learningPath.value) as LearningPath) : learningPath.value,
: learningPath.value
); );
watch(() => props.selectedLearningPath, () => learningPath.value = props.selectedLearningPath ?? DEFAULT_LEARNING_PATH); watch(
() => props.selectedLearningPath,
() => (learningPath.value = props.selectedLearningPath ?? DEFAULT_LEARNING_PATH),
);
function uploadLearningPath(): void { function uploadLearningPath(): void {
if (props.selectedLearningPath) { if (props.selectedLearningPath) {
@ -69,20 +75,20 @@ import { parse } from 'uuid';
if (props.selectedLearningPath) { if (props.selectedLearningPath) {
mutate({ mutate({
hruid: props.selectedLearningPath.hruid, hruid: props.selectedLearningPath.hruid,
language: props.selectedLearningPath.language as Language language: props.selectedLearningPath.language as Language,
}); });
} }
} }
function extractErrorMessage(error: AxiosError): string { function extractErrorMessage(error: AxiosError): string {
return (error.response?.data as {error: string}).error ?? error.message; return (error.response?.data as { error: string }).error ?? error.message;
} }
const isIdModified = computed(() => const isIdModified = computed(
props.selectedLearningPath !== undefined && ( () =>
props.selectedLearningPath.hruid !== parsedLearningPath.value.hruid props.selectedLearningPath !== undefined &&
|| props.selectedLearningPath.language !== parsedLearningPath.value.language (props.selectedLearningPath.hruid !== parsedLearningPath.value.hruid ||
) props.selectedLearningPath.language !== parsedLearningPath.value.language),
); );
function getErrorMessage(): string | null { function getErrorMessage(): string | null {
@ -93,24 +99,22 @@ import { parse } from 'uuid';
} else if (deleteError.value) { } else if (deleteError.value) {
return t(extractErrorMessage(deleteError.value)); return t(extractErrorMessage(deleteError.value));
} else if (isIdModified.value) { } else if (isIdModified.value) {
return t('learningPathCantModifyId'); return t("learningPathCantModifyId");
} }
return null; return null;
} }
</script> </script>
<template> <template>
<v-card <v-card :title="props.selectedLearningPath ? t('editLearningPath') : t('newLearningPath')">
:title="props.selectedLearningPath ? t('editLearningPath') : t('newLearningPath')"
>
<template v-slot:text> <template v-slot:text>
<json-editor-vue v-model="learningPath"></json-editor-vue> <json-editor-vue v-model="learningPath"></json-editor-vue>
<v-alert <v-alert
v-if="postError || putError || deleteError || isIdModified" v-if="postError || putError || deleteError || isIdModified"
icon="mdi mdi-alert-circle" icon="mdi mdi-alert-circle"
type="error" type="error"
:title="t('error')" :title="t('error')"
:text="getErrorMessage()!" :text="getErrorMessage()!"
></v-alert> ></v-alert>
</template> </template>
<template v-slot:actions> <template v-slot:actions>
@ -120,7 +124,7 @@ import { parse } from 'uuid';
:loading="isPostPending || isPutPending" :loading="isPostPending || isPutPending"
:disabled="parsedLearningPath.hruid === DEFAULT_LEARNING_PATH.hruid || isIdModified" :disabled="parsedLearningPath.hruid === DEFAULT_LEARNING_PATH.hruid || isIdModified"
> >
{{ props.selectedLearningPath ? t('saveChanges') : t('create') }} {{ props.selectedLearningPath ? t("saveChanges") : t("create") }}
</v-btn> </v-btn>
<button-with-confirmation <button-with-confirmation
@confirm="deleteLearningPath" @confirm="deleteLearningPath"
@ -136,11 +140,10 @@ import { parse } from 'uuid';
prepend-icon="mdi mdi-open-in-new" prepend-icon="mdi mdi-open-in-new"
:disabled="!props.selectedLearningPath" :disabled="!props.selectedLearningPath"
> >
{{ t('open') }} {{ t("open") }}
</v-btn> </v-btn>
</template> </template>
</v-card> </v-card>
</template> </template>
<style scoped> <style scoped></style>
</style>

View file

@ -1,27 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningPathPreviewCard from './LearningPathPreviewCard.vue'; import LearningPathPreviewCard from "./LearningPathPreviewCard.vue";
import { computed, ref, watch, type Ref } from 'vue'; import { computed, ref, watch, type Ref } from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from "vue-i18n";
import type { LearningPath as LearningPathDTO } from '@dwengo-1/common/interfaces/learning-content'; import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
learningPaths: LearningPathDTO[] learningPaths: LearningPathDTO[];
}>(); }>();
const tableHeaders = [ const tableHeaders = [
{ title: t("hruid"), width: "250px", key: "hruid" }, { title: t("hruid"), width: "250px", key: "hruid" },
{ title: t("language"), width: "50px", key: "language" }, { title: t("language"), width: "50px", key: "language" },
{ title: t("title"), key: "title" } { title: t("title"), key: "title" },
]; ];
const selectedLearningPaths: Ref<LearningPathDTO[]> = ref([]); const selectedLearningPaths: Ref<LearningPathDTO[]> = ref([]);
const selectedLearningPath = computed(() => const selectedLearningPath = computed(() =>
selectedLearningPaths.value ? selectedLearningPaths.value[0] : undefined selectedLearningPaths.value ? selectedLearningPaths.value[0] : undefined,
); );
watch(() => props.learningPaths, () => selectedLearningPaths.value = []); watch(
() => props.learningPaths,
() => (selectedLearningPaths.value = []),
);
</script> </script>
<template> <template>
@ -38,7 +41,10 @@
/> />
</div> </div>
<div class="preview-container"> <div class="preview-container">
<learning-path-preview-card class="preview" :selectedLearningPath="selectedLearningPath"/> <learning-path-preview-card
class="preview"
:selectedLearningPath="selectedLearningPath"
/>
</div> </div>
</div> </div>
</template> </template>