diff options
| -rw-r--r-- | lib/classes/JsonApi/RouteMap.php | 12 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Courseware/CustomFilesCreate.php | 46 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Courseware/CustomFilesDelete.php | 36 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Courseware/CustomFilesList.php | 40 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Courseware/CustomFilesShow.php | 38 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Courseware/CustomFilesUpdate.php | 42 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Courseware/CustomFilesUpdateAttributes.php | 47 | ||||
| -rw-r--r-- | lib/classes/JsonApi/SchemaMap.php | 1 | ||||
| -rwxr-xr-x | lib/classes/JsonApi/Schemas/Courseware/CustomFile.php | 61 | ||||
| -rw-r--r-- | lib/models/Courseware/CustomFiles.php | 69 | ||||
| -rw-r--r-- | lib/models/Courseware/Filesystem/CustomFile.php | 123 | ||||
| -rw-r--r-- | resources/vue/mixins/courseware/export.js | 30 | ||||
| -rw-r--r-- | resources/vue/mixins/courseware/import.js | 51 | ||||
| -rw-r--r-- | resources/vue/store/courseware/courseware.module.js | 36 |
14 files changed, 629 insertions, 3 deletions
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 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Block; +use Courseware\Filesystem\CustomFile; +use JsonApi\Routes\Files\RoutesHelperTrait; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Create a block in a container. + */ +class CustomFilesCreate extends JsonApiController +{ + use RoutesHelperTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + if (!($resource = Block::find($args['id']))) { + throw new RecordNotFoundException(); + } + + if (!Authority::canUpdateBlock($this->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 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Block; +use JsonApi\Routes\Files\RoutesHelperTrait; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Create a block in a container. + */ +class CustomFilesDelete extends JsonApiController +{ + use RoutesHelperTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + if (!($resource = Block::find($args['id']))) { + throw new RecordNotFoundException(); + } + + if (!Authority::canUpdateBlock($user = $this->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 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Block; +use Courseware\CustomFiles; +use JsonApi\Routes\Files\RoutesHelperTrait; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Create a block in a container. + */ +class CustomFilesList extends JsonApiController +{ + use RoutesHelperTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + if (!($resource = Block::find($args['id']))) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowBlock($user = $this->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 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Block; +use GuzzleHttp\Psr7; +use JsonApi\Routes\Files\RoutesHelperTrait; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\NonJsonApiController; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Create a block in a container. + */ +class CustomFilesShow extends NonJsonApiController +{ + use RoutesHelperTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + if (!($resource = Block::find($args['id']))) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowBlock($user = $this->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 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Block; +use JsonApi\Routes\Files\RoutesHelperTrait; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\NonJsonApiController; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Create a block in a container. + */ +class CustomFilesUpdate extends NonJsonApiController +{ + use RoutesHelperTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + if (!($resource = Block::find($args['id']))) { + throw new RecordNotFoundException(); + } + + if (!Authority::canUpdateBlock($user = $this->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 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Block; +use Courseware\Filesystem\CustomFile; +use JsonApi\Routes\Files\RoutesHelperTrait; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Create a block in a container. + */ +class CustomFilesUpdateAttributes extends JsonApiController +{ + use RoutesHelperTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + if (!($resource = Block::find($args['id']))) { + throw new RecordNotFoundException(); + } + + if (!Authority::canUpdateBlock($user = $this->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 @@ +<?php + +namespace JsonApi\Schemas\Courseware; + +use JsonApi\Schemas\SchemaProvider; +use Neomerx\JsonApi\Contracts\Schema\ContextInterface; +use Neomerx\JsonApi\Schema\Link; +use Neomerx\JsonApi\Contracts\Schema\LinkInterface; + +class CustomFile extends SchemaProvider +{ + const TYPE = 'courseware-custom-file'; + //const REL_CUSTOM_FILE = 'courseware-custom-file'; + + /** + * {@inheritdoc} + */ + public function getId($resource): ?string + { + return $resource->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 @@ +<?php + +namespace Courseware; + +use Courseware\Filesystem\CustomFile; + +/** + * This interface enables a courseware-block to have a user defined representation + * of files. This enables a block to have its own internal representation for + * arbitrary content as well allowing the import and export of said content in + * a defined and coherent way. + */ +interface CustomFiles +{ + /** + * Returns an array of CustomFile objects belongig to this block + * + * @return array<int, CustomFile> + */ + 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 @@ +<?php + +namespace Courseware\Filesystem; + +/** + * This class represents the metdata for a custom file in a courseware block + */ +class CustomFile +{ + /** + * The payload for this custom file, containt id, block_id and arbitrary attributes + * @var [type] + */ + protected + $payload; + + /** + * create a new custom file object, containing a self assigned id (make it unique!), + * the blocks id this custom file is referenced to and some attributes of + * your choice + * + * @param string $id an unique id for this custom file + * @param int $block_id the id of the related block + * @param array $attributes [description] + */ + public function __construct($id = null, $block_id, $attributes = []) + { + $this->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( |
