feat(backend): opvragen van leerobjecten van een leerkracht
This commit is contained in:
parent
78353d6b65
commit
6600441b08
11 changed files with 152 additions and 38 deletions
|
@ -8,6 +8,7 @@ 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";
|
||||||
|
|
||||||
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO {
|
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO {
|
||||||
if (!req.params.hruid) {
|
if (!req.params.hruid) {
|
||||||
|
@ -31,6 +32,12 @@ 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.
|
||||||
|
const learningObjects =
|
||||||
|
await learningObjectService.getLearningObjectsAdministratedBy(req.query.admin as string);
|
||||||
|
|
||||||
|
res.json(learningObjects);
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
@ -42,6 +49,7 @@ export async function getAllLearningObjects(req: Request, res: Response): Promis
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ learningObjects: learningObjects });
|
res.json({ learningObjects: learningObjects });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLearningObject(req: Request, res: Response): Promise<void> {
|
export async function getLearningObject(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -74,9 +82,13 @@ export async function getAttachment(req: Request, res: Response): Promise<void>
|
||||||
res.setHeader('Content-Type', attachment.mimeType).send(attachment.content);
|
res.setHeader('Content-Type', attachment.mimeType).send(attachment.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handlePostLearningObject(req: Request, res: Response): Promise<void> {
|
export async function handlePostLearningObject(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||||
if (!req.files || !req.files[0]) {
|
if (!req.files || !req.files.learningObject) {
|
||||||
throw new BadRequestException('No file uploaded');
|
throw new BadRequestException('No file uploaded');
|
||||||
}
|
}
|
||||||
await learningObjectService.storeLearningObject((req.files[0] as UploadedFile).tempFilePath);
|
const learningObject = await learningObjectService.storeLearningObject(
|
||||||
|
(req.files.learningObject as UploadedFile).tempFilePath,
|
||||||
|
[req.auth!.username]
|
||||||
|
);
|
||||||
|
res.json(learningObject);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,9 +33,9 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
public async findAllByAdmin(adminUsername: string): Promise<LearningObject[]> {
|
||||||
return this.find(
|
return this.find(
|
||||||
{ admins: teacher },
|
{ admins: { $contains: adminUsername } },
|
||||||
{ populate: ['admins'] } // Make sure to load admin relations
|
{ populate: ['admins'] } // Make sure to load admin relations
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,14 @@
|
||||||
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 { 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';
|
||||||
|
@ -28,7 +38,7 @@ export class LearningObject {
|
||||||
@ManyToMany({
|
@ManyToMany({
|
||||||
entity: () => Teacher,
|
entity: () => Teacher,
|
||||||
})
|
})
|
||||||
admins!: Teacher[];
|
admins: Collection<Teacher> = new Collection<Teacher>(this);
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
title!: string;
|
title!: string;
|
||||||
|
@ -84,7 +94,7 @@ export class LearningObject {
|
||||||
entity: () => Attachment,
|
entity: () => Attachment,
|
||||||
mappedBy: 'learningObject',
|
mappedBy: 'learningObject',
|
||||||
})
|
})
|
||||||
attachments: Attachment[] = [];
|
attachments: Collection<Attachment> = new Collection<Attachment>(this);
|
||||||
|
|
||||||
@Property({ type: 'blob' })
|
@Property({ type: 'blob' })
|
||||||
content!: Buffer;
|
content!: Buffer;
|
||||||
|
|
|
@ -109,6 +109,17 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
|
||||||
);
|
);
|
||||||
return learningObjects.filter((it) => it !== null);
|
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;
|
export default databaseLearningObjectProvider;
|
||||||
|
|
|
@ -135,6 +135,13 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
|
||||||
|
|
||||||
return html;
|
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;
|
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).
|
* Obtain a HTML-rendering of the learning object with the given identifier (as a string).
|
||||||
*/
|
*/
|
||||||
getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null>;
|
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[]>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,10 @@ import {
|
||||||
LearningObjectIdentifierDTO,
|
LearningObjectIdentifierDTO,
|
||||||
LearningPathIdentifier
|
LearningPathIdentifier
|
||||||
} from '@dwengo-1/common/interfaces/learning-content';
|
} from '@dwengo-1/common/interfaces/learning-content';
|
||||||
import {getLearningObjectRepository} from "../../data/repositories";
|
import {getLearningObjectRepository, getTeacherRepository} from "../../data/repositories";
|
||||||
import {processLearningObjectZip} from "./learning-object-zip-processing-service";
|
import {processLearningObjectZip} from "./learning-object-zip-processing-service";
|
||||||
import {BadRequestException} from "../../exceptions/bad-request-exception";
|
import {BadRequestException} from "../../exceptions/bad-request-exception";
|
||||||
|
import {LearningObject} from "../../entities/content/learning-object.entity";
|
||||||
|
|
||||||
function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider {
|
function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider {
|
||||||
if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
|
if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
|
||||||
|
@ -50,19 +51,40 @@ const learningObjectService = {
|
||||||
return getProvider(id).getLearningObjectHTML(id);
|
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.
|
* 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): Promise<void> {
|
async storeLearningObject(learningObjectPath: string, admins: string[]): Promise<LearningObject> {
|
||||||
const learningObjectRepository = getLearningObjectRepository();
|
const learningObjectRepository = getLearningObjectRepository();
|
||||||
const learningObject = await processLearningObjectZip(learningObjectPath);
|
const learningObject = await processLearningObjectZip(learningObjectPath);
|
||||||
|
|
||||||
|
console.log(learningObject);
|
||||||
if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
|
if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
|
||||||
throw new BadRequestException("Learning object name must start with the user content prefix!");
|
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(it => teacherRepo.findByUsername(it))
|
||||||
|
);
|
||||||
|
adminTeachers.forEach(it => {
|
||||||
|
if (it != null) {
|
||||||
|
learningObject.admins.add(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await learningObjectRepository.save(learningObject, {preventOverwrite: true});
|
await learningObjectRepository.save(learningObject, {preventOverwrite: true});
|
||||||
|
return learningObject;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,9 @@ import {getAttachmentRepository, getLearningObjectRepository} from "../../data/r
|
||||||
import {BadRequestException} from "../../exceptions/bad-request-exception";
|
import {BadRequestException} from "../../exceptions/bad-request-exception";
|
||||||
import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content";
|
import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content";
|
||||||
|
|
||||||
|
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.
|
* Process an uploaded zip file and construct a LearningObject from its contents.
|
||||||
* @param filePath Path of the zip file to process.
|
* @param filePath Path of the zip file to process.
|
||||||
|
@ -15,16 +18,19 @@ export async function processLearningObjectZip(filePath: string): Promise<Learni
|
||||||
|
|
||||||
const zip = await unzipper.Open.file(filePath);
|
const zip = await unzipper.Open.file(filePath);
|
||||||
|
|
||||||
let metadata: LearningObjectMetadata | null = null;
|
let metadata: LearningObjectMetadata | undefined = undefined;
|
||||||
const attachments: {name: string, content: Buffer}[] = [];
|
const attachments: {name: string, content: Buffer}[] = [];
|
||||||
let content: Buffer | null = null;
|
let content: Buffer | undefined = undefined;
|
||||||
|
|
||||||
|
if (zip.files.length == 0) {
|
||||||
|
throw new BadRequestException("empty_zip")
|
||||||
|
}
|
||||||
|
|
||||||
for (const file of zip.files) {
|
for (const file of zip.files) {
|
||||||
if (file.type === "Directory") {
|
if (file.type !== "Directory") {
|
||||||
throw new BadRequestException("The learning object zip file should not contain directories.");
|
if (METADATA_PATH_REGEX.test(file.path)) {
|
||||||
} else if (file.path === "metadata.json") {
|
|
||||||
metadata = await processMetadataJson(file);
|
metadata = await processMetadataJson(file);
|
||||||
} else if (file.path.startsWith("index.")) {
|
} else if (CONTENT_PATH_REGEX.test(file.path)) {
|
||||||
content = await processFile(file);
|
content = await processFile(file);
|
||||||
} else {
|
} else {
|
||||||
attachments.push({
|
attachments.push({
|
||||||
|
@ -33,22 +39,41 @@ export async function processLearningObjectZip(filePath: string): Promise<Learni
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
throw new BadRequestException("Missing metadata.json file");
|
throw new BadRequestException("missing_metadata");
|
||||||
}
|
}
|
||||||
if (!content) {
|
if (!content) {
|
||||||
throw new BadRequestException("Missing index file");
|
throw new BadRequestException("missing_index");
|
||||||
}
|
}
|
||||||
|
|
||||||
const learningObject = learningObjectRepo.create(metadata);
|
|
||||||
|
const learningObject = learningObjectRepo.create({
|
||||||
|
admins: [],
|
||||||
|
available: metadata.available ?? true,
|
||||||
|
content: content,
|
||||||
|
contentType: metadata.content_type,
|
||||||
|
copyright: metadata.copyright,
|
||||||
|
description: metadata.description,
|
||||||
|
educationalGoals: metadata.educational_goals,
|
||||||
|
hruid: metadata.hruid,
|
||||||
|
keywords: metadata.keywords,
|
||||||
|
language: metadata.language,
|
||||||
|
license: "",
|
||||||
|
returnValue: metadata.return_value,
|
||||||
|
skosConcepts: metadata.skos_concepts,
|
||||||
|
teacherExclusive: metadata.teacher_exclusive,
|
||||||
|
title: metadata.title,
|
||||||
|
version: metadata.version
|
||||||
|
});
|
||||||
const attachmentEntities = attachments.map(it => attachmentRepo.create({
|
const attachmentEntities = attachments.map(it => attachmentRepo.create({
|
||||||
name: it.name,
|
name: it.name,
|
||||||
content: it.content,
|
content: it.content,
|
||||||
mimeType: mime.lookup(it.name) || "text/plain",
|
mimeType: mime.lookup(it.name) || "text/plain",
|
||||||
learningObject
|
learningObject
|
||||||
}))
|
}))
|
||||||
learningObject.attachments.push(...attachmentEntities);
|
attachmentEntities.forEach(it => learningObject.attachments.add(it));
|
||||||
|
|
||||||
return learningObject;
|
return learningObject;
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ export async function getTeacherQuestions(username: string, full: boolean): Prom
|
||||||
|
|
||||||
// Find all learning objects that this teacher manages
|
// Find all learning objects that this teacher manages
|
||||||
const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository();
|
const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository();
|
||||||
const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher);
|
const learningObjects: LearningObject[] = await learningObjectRepository.findAllByAdmin(teacher);
|
||||||
|
|
||||||
if (!learningObjects || learningObjects.length === 0) {
|
if (!learningObjects || learningObjects.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
Loading…
Add table
Add a link
Reference in a new issue