From da3db0c846bd2c3498a487727ddb11b37324824f Mon Sep 17 00:00:00 2001 From: tgloeggl Date: Thu, 13 Jan 2022 14:38:39 +0100 Subject: working on creating plugin-api for im- and export fix bug fix passing of custom file to block fix custom-file creation on client side working on crud custom file api for courseware blocks create correct link for custom file add read part of CRUD add delete part of CRUD change behaviour of add a new file, contents are passed via separate route update custom-file content add route to patch metadata of custom-file changes to export for reference fix jsonapi for custom-files export custom-files correctly import custom files remove custom files from file-refs revert changes to block remove obsolte use statement in Block revert changes to BlockType and Download-Block remove obsolete implements from DownloadBlock add doku, fix exporting of blocks without custom-files remove debugging messages and now obsolete code cleaning up rework custom file check fix error and remove call to mime-type for custom files rename interface methods do not assign variables in function calls --- lib/classes/JsonApi/RouteMap.php | 12 ++ .../Routes/Courseware/CustomFilesCreate.php | 46 ++++++++ .../Routes/Courseware/CustomFilesDelete.php | 36 ++++++ .../JsonApi/Routes/Courseware/CustomFilesList.php | 40 +++++++ .../JsonApi/Routes/Courseware/CustomFilesShow.php | 38 +++++++ .../Routes/Courseware/CustomFilesUpdate.php | 42 +++++++ .../Courseware/CustomFilesUpdateAttributes.php | 47 ++++++++ lib/classes/JsonApi/SchemaMap.php | 1 + .../JsonApi/Schemas/Courseware/CustomFile.php | 61 ++++++++++ lib/models/Courseware/CustomFiles.php | 69 ++++++++++++ lib/models/Courseware/Filesystem/CustomFile.php | 123 +++++++++++++++++++++ resources/vue/mixins/courseware/export.js | 30 +++++ resources/vue/mixins/courseware/import.js | 51 ++++++++- .../vue/store/courseware/courseware.module.js | 36 ++++++ 14 files changed, 629 insertions(+), 3 deletions(-) create mode 100644 lib/classes/JsonApi/Routes/Courseware/CustomFilesCreate.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/CustomFilesDelete.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/CustomFilesList.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/CustomFilesShow.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/CustomFilesUpdate.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/CustomFilesUpdateAttributes.php create mode 100755 lib/classes/JsonApi/Schemas/Courseware/CustomFile.php create mode 100644 lib/models/Courseware/CustomFiles.php create mode 100644 lib/models/Courseware/Filesystem/CustomFile.php diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index baccc26..03dce11 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -339,6 +339,18 @@ class RouteMap // not a JSON route $group->post('/courseware-blocks/{id}/copy', Routes\Courseware\BlocksCopy::class); + // routes for custom files + + // get all referenced custom files + $group->get('/courseware-blocks/{id}/custom-files', Routes\Courseware\CustomFilesList::class); + + // CRUD routes for single custom files + $group->get('/courseware-blocks/{id}/custom-files/{file_id}', Routes\Courseware\CustomFilesShow::class); + $group->post('/courseware-blocks/{id}/custom-files', Routes\Courseware\CustomFilesCreate::class); + $group->patch('/courseware-blocks/{id}/custom-files/{file_id}', Routes\Courseware\CustomFilesUpdateAttributes::class); + $group->post('/courseware-blocks/{id}/custom-files/{file_id}', Routes\Courseware\CustomFilesUpdate::class); + $group->delete('/courseware-blocks/{id}/custom-files/{file_id}', Routes\Courseware\CustomFilesDelete::class); + $group->get('/courseware-containers/{id}', Routes\Courseware\ContainersShow::class); $group->post('/courseware-containers', Routes\Courseware\ContainersCreate::class); $group->patch('/courseware-containers/{id}', Routes\Courseware\ContainersUpdate::class); diff --git a/lib/classes/JsonApi/Routes/Courseware/CustomFilesCreate.php b/lib/classes/JsonApi/Routes/Courseware/CustomFilesCreate.php new file mode 100644 index 0000000..56615b3 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/CustomFilesCreate.php @@ -0,0 +1,46 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + $body = $request->getParsedBody(); + + $custom_file = new CustomFile( + null, + $args['id'], + $body['data']['attributes'] + ); + + return $this->getContentResponse( + $resource->type->createCustomFile($custom_file) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/CustomFilesDelete.php b/lib/classes/JsonApi/Routes/Courseware/CustomFilesDelete.php new file mode 100644 index 0000000..f7e005d --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/CustomFilesDelete.php @@ -0,0 +1,36 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + $resource->type->deleteCustomFile($args['id']); + return $response; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/CustomFilesList.php b/lib/classes/JsonApi/Routes/Courseware/CustomFilesList.php new file mode 100644 index 0000000..1d776c0 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/CustomFilesList.php @@ -0,0 +1,40 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + if (!$resource->type instanceof CustomFiles) { + return $response; + } + + return $this->getContentResponse($resource->type->getCustomFiles()); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/CustomFilesShow.php b/lib/classes/JsonApi/Routes/Courseware/CustomFilesShow.php new file mode 100644 index 0000000..0828bb7 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/CustomFilesShow.php @@ -0,0 +1,38 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + return $response->withBody( + $stream = Psr7\stream_for($resource->type->readCustomFile($args['id'])) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/CustomFilesUpdate.php b/lib/classes/JsonApi/Routes/Courseware/CustomFilesUpdate.php new file mode 100644 index 0000000..72f3d4e --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/CustomFilesUpdate.php @@ -0,0 +1,42 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + $uploadedFile = $this->getUploadedFile($request); + + $resource->type->updateCustomFileContent( + $args['file_id'], + $content = file_get_contents($uploadedFile->getFilepath()) + ); + + return $response; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/CustomFilesUpdateAttributes.php b/lib/classes/JsonApi/Routes/Courseware/CustomFilesUpdateAttributes.php new file mode 100644 index 0000000..cd1100b --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/CustomFilesUpdateAttributes.php @@ -0,0 +1,47 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + $body = $request->getParsedBody(); + + $custom_file = new CustomFile( + $body['data']['id'], + $args['id'], + $body['data']['attributes'] + ); + + return $this->getContentResponse( + $resource->type->updateCustomFileMetadata( + $args['file_id'], $custom_file) + ); + } +} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index e7168cd..839962d 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -55,6 +55,7 @@ class SchemaMap \Courseware\BlockComment::class => Schemas\Courseware\BlockComment::class, \Courseware\BlockFeedback::class => Schemas\Courseware\BlockFeedback::class, \Courseware\Container::class => Schemas\Courseware\Container::class, + \Courseware\Filesystem\CustomFile::class => Schemas\Courseware\CustomFile::class, \Courseware\Instance::class => Schemas\Courseware\Instance::class, \Courseware\StructuralElement::class => Schemas\Courseware\StructuralElement::class, \Courseware\StructuralElementComment::class => Schemas\Courseware\StructuralElementComment::class, diff --git a/lib/classes/JsonApi/Schemas/Courseware/CustomFile.php b/lib/classes/JsonApi/Schemas/Courseware/CustomFile.php new file mode 100755 index 0000000..21105bc --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Courseware/CustomFile.php @@ -0,0 +1,61 @@ +getPayload()['id']; + } + + /** + * {@inheritdoc} + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return $resource->getPayload(); + } + + /** + * {@inheritdoc} + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + + return []; + } + + public function getSelfLink($resource): LinkInterface + { + $link = new Link(true, '/courseware-blocks/' . $resource->getBlockId() + .'/custom-files', false); + return $link; + } + + /** + * @inheritdoc + */ + public function hasResourceMeta($resource): bool + { + return true; + } + + public function getResourceMeta($resource) + { + return [ + 'download-url' => $resource->getDownloadUrl() + ]; + } +} diff --git a/lib/models/Courseware/CustomFiles.php b/lib/models/Courseware/CustomFiles.php new file mode 100644 index 0000000..21a0ea6 --- /dev/null +++ b/lib/models/Courseware/CustomFiles.php @@ -0,0 +1,69 @@ + + */ + public function getCustomFiles() : array; + + /** + * create a new custom file, the contents have to be set by updateCustomFilesContent afterwards + * + * @param array $metadata any additional metadata needed, like id + * @param string $content the files contets + * + * @return CustomFile the newly created custom file + */ + public function createCustomFile(CustomFile $custom_file) : CustomFile; + + /** + * returns the contents for the custom file with the passed id + * + * @param string $id the id for the custom file + * + * @return string + */ + public function readCustomFile($id) : string; + + /** + * update the attributes of the custom file for the passed id + * + * @param string $id + * @param array $metadata + * + * @return CustomFile + */ + public function updateCustomFileMetadata($id, CustomFile $custom_file) : CustomFile; + + /** + * update the contents of the customf ile for the passed id + * + * @param string $id + * @param string $content + * + * @return CustomFile + */ + public function updateCustomFileContent($id, $content) : CustomFile; + + /** + * delete the custom file for the passed id + * + * @param string $id + * + * @return bool + */ + public function deleteCustomFile($id) : bool; +} diff --git a/lib/models/Courseware/Filesystem/CustomFile.php b/lib/models/Courseware/Filesystem/CustomFile.php new file mode 100644 index 0000000..7a6d4b8 --- /dev/null +++ b/lib/models/Courseware/Filesystem/CustomFile.php @@ -0,0 +1,123 @@ +payload = [ + 'id' => $id, + 'block_id' => $block_id, + 'attributes' => $attributes + ]; + } + + /** + * returns the payload: [ + * 'id' => ..., + * 'block_id' => ..., + * 'attributes' => [ ... ] + * ] + * + * @return array the payload + */ + public function getPayload() + { + return $this->payload; + } + + /** + * get id for this custom file + * + * @return string custom file id + */ + public function getId() + { + return $this->payload['id']; + } + + /** + * get id of related block + * + * @return int related block id + */ + public function getBlockId() + { + return $this->payload['block_id']; + } + + /** + * Overwrite the complete payload for this block. The payload MUST have the + * following structure: [ + * 'id' => ..., + * 'block_id' => ..., + * 'attributes' => [ ... ] + * ] + * + * @param array $payload the payload of appropriate structure + */ + public function setPayload($payload): void + { + if (!$payload['id']) { + throw new InvalidArgumentException(); + } + + if (!$payload['block_id']) { + throw new InvalidArgumentException(); + } + + $this->payload = $payload; + } + + /** + * Set the unique id for this custom file + * + * @param string $id + */ + public function setId($id): void + { + $this->payload['id'] = $id; + } + + /** + * Set the id for the related block + * + * @param int $block_id + */ + public function setBlockId($block_id): void + { + $this->payload['block_id'] = $block_id; + } + + /** + * Get the download url for this custom file + * + * @return string the download url + */ + public function getDownloadUrl() : string + { + return rtrim(\URLHelper::getUrl('jsonapi.php/v1'), '/') + . '/courseware-blocks/' . $this->payload['block_id'] + . '/custom-files/'. $this->payload['id']; + } +} diff --git a/resources/vue/mixins/courseware/export.js b/resources/vue/mixins/courseware/export.js index f15c314..d4f6bd0 100644 --- a/resources/vue/mixins/courseware/export.js +++ b/resources/vue/mixins/courseware/export.js @@ -286,6 +286,7 @@ export default { // export file data (if any) if (block_ref.relationships['file-refs']?.links?.related) { await this.exportFileRefs(block_ref.id); + await this.exportCustomFiles(block_ref.id); } delete block.relationships; @@ -293,6 +294,34 @@ export default { return block; }, + async exportCustomFiles(block_id) { + // load export data + let refs = [] + try { + refs = await this.loadCustomFiles(block_id); + } catch(e) { + //TODO: Companion explains error + } + + for (let ref_id in refs) { + console.log('custom-file-ref', refs[ref_id]); + + let attributes = refs[ref_id].attributes; + delete attributes.content; + + this.exportFiles.json.push({ + 'id' : refs[ref_id].id, + 'attributes' : refs[ref_id].attributes, + 'related_block_id' : block_id, + 'type' : 'custom-file' + }); + + this.exportFiles.download[refs[ref_id].id] = { + url: refs[ref_id].meta['download-url'] + }; + } + }, + async exportFileRefs(block_id) { // load file-ref data let refs = [] @@ -345,6 +374,7 @@ export default { ...mapActions([ 'loadStructuralElement', 'loadFileRefs', + 'loadCustomFiles', 'loadFolder', 'companionInfo', 'setExportState', diff --git a/resources/vue/mixins/courseware/import.js b/resources/vue/mixins/courseware/import.js index 100a0e5..3a79108 100644 --- a/resources/vue/mixins/courseware/import.js +++ b/resources/vue/mixins/courseware/import.js @@ -9,6 +9,7 @@ export default { elementCounter: 0, importElementCounter: 0, currentImportErrors: [], + customFiles: [] }; }, @@ -246,7 +247,7 @@ export default { // update old id ids in payload part for (var i = 0; i < files.length; i++) { - if (files[i].related_block_id === block.id) { + if (files[i].related_block_id === block.id && files[i].type === undefined) { let old_file = this.file_mapping[files[i].id].old; let new_file = this.file_mapping[files[i].id].new; let payload = JSON.stringify(block.attributes.payload); @@ -274,6 +275,24 @@ export default { } + this.setImportFilesProgress(0); + this.setImportFilesState(''); + + for (var i = 0; i < this.customFiles.length; i++) { + if (this.customFiles[i].related_block_id == block.id) { + await this.createCustomFile({ + file: this.customFiles[i], + block_id: new_block.id + }); + } + + this.setImportFilesState(this.$gettext('Erzeuge Datei')); + this.setImportFilesProgress(parseInt(i / this.customFiles.length * 100)); + } + + this.setImportFilesProgress(100); + this.setImportFilesState(''); + return new_block; }, @@ -333,11 +352,14 @@ export default { // upload all files to the newly created folder if (main_folder) { for (var i = 0; i < files.length; i++) { + let custom = files[i].type === 'custom-file'; + // if the subfolder with the referenced id does not exist yet, create it if (!files[i].folder) { continue; } - if (!folders[files[i].folder.id]) { + + if (!custom && !folders[files[i].folder.id]) { this.setImportFilesState(this.$gettext('Lege Ordner an') + ': ' + files[i].folder.name); folders[files[i].folder.id] = await this.createFolder({ context: this.context, @@ -355,7 +377,29 @@ export default { } // only upload files with the same id once - if (this.file_mapping[files[i].id] === undefined) { + if (custom) { + let zip_filedata = await this.zip.file(files[i].id).async('blob'); + + // create new blob with correct type + let filedata = zip_filedata.slice(0, zip_filedata.size); + + this.setImportFilesState(this.$gettext('Erzeuge Datei')); + + + let file = await this.createCustomFile({ + 'file': files[i], + 'filedata' : filedata, + 'block_id' : files[i].attributes['block_id'] + }); + + //file mapping + this.file_mapping[files[i].id] = { + old: files[i], + new: file + }; + + this.setImportFilesProgress(parseInt(i / files.length * 100)); + } else if (this.file_mapping[files[i].id] === undefined) { let zip_filedata = await this.zip.file(files[i].id).async('blob'); // create new blob with correct type @@ -394,6 +438,7 @@ export default { 'createFolder', 'createRootFolder', 'createFile', + 'createCustomFile', 'lockObject', 'unlockObject', 'setImportFilesState', diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index 49ac520..59a94a7 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -305,6 +305,42 @@ export const actions = { }); }, + async createCustomFile(context, { file, filedata, block_id }) { + // create custom file for block + let url = `courseware-blocks/${block_id}/custom-files`; + let newFile = await state.httpClient.post(url, file). + then(({data }) => { + return data.data; + }); + + // set file data with separate call + let formData = new FormData(); + formData.append('file', filedata, newFile.id); + + url = `courseware-blocks/${block_id}/custom-files/${newFile.id}`; + + await state.httpClient.post(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return newFile; + }, + + async loadCustomFiles(context, block_id) { + const parent = { + type: 'courseware-blocks', + id: block_id, + }; + + const url = `courseware-blocks/${block_id}/custom-files` + return state.httpClient.get(url) + .then(({ data }) => { + return data.data; + }); + }, + async createRootFolder({ dispatch, rootGetters }, { context, folder }) { // get root folder for this context await dispatch( -- cgit v1.0