diff --git a/app/controllers/jsupdater.php b/app/controllers/jsupdater.php index 448230ba118e3b0cc9ac06a4551465ca860b44fa..d0cd7219c5796b6113921c6cf9e6ff89415b4d48 100644 --- a/app/controllers/jsupdater.php +++ b/app/controllers/jsupdater.php @@ -113,6 +113,7 @@ class JsupdaterController extends AuthenticatedController { $pageInfo = Request::getArray("page_info"); $data = [ + 'coursewareclipboard' => $this->getCoursewareClipboardUpdates($pageInfo), 'blubber' => $this->getBlubberUpdates($pageInfo), 'messages' => $this->getMessagesUpdates($pageInfo), 'personalnotifications' => $this->getPersonalNotificationUpdates($pageInfo), @@ -258,4 +259,9 @@ class JsupdaterController extends AuthenticatedController return $data; } + + private function getCoursewareClipboardUpdates($pageInfo) + { + return count(\Courseware\Clipboard::findUsersClipboards($GLOBALS["user"])) != $pageInfo['coursewareclipboard']['counter']; + } } diff --git a/db/migrations/5.4.5_create_cw_clipboards_table.php b/db/migrations/5.4.5_create_cw_clipboards_table.php new file mode 100644 index 0000000000000000000000000000000000000000..02fc9613a9d1146e3af8c3b3b9a1a40f90c9fce5 --- /dev/null +++ b/db/migrations/5.4.5_create_cw_clipboards_table.php @@ -0,0 +1,39 @@ +<?php + +class CreateCwClipboardsTable extends Migration +{ + public function description() + { + return 'create table for courseware clipboards'; + } + + public function up() + { + $db = DBManager::get(); + + $query = "CREATE TABLE IF NOT EXISTS `cw_clipboards` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `user_id` CHAR(32) COLLATE latin1_bin NOT NULL, + `name` VARCHAR(255) NOT NULL, + `description` MEDIUMTEXT NOT NULL, + `block_id` INT(11) NULL, + `container_id` INT(11) NULL, + `structural_element_id` INT(11) NULL, + `object_type` ENUM('courseware-structural-elements', 'courseware-containers', 'courseware-blocks') COLLATE latin1_bin NOT NULL, + `object_kind` VARCHAR(255) COLLATE latin1_bin NOT NULL, + `backup` MEDIUMTEXT NOT NULL, + `mkdate` INT(11) UNSIGNED NOT NULL, + `chdate` INT(11) UNSIGNED NOT NULL, + + PRIMARY KEY (`id`), + INDEX index_user_id (`user_id`) + )"; + $db->exec($query); + } + + public function down() + { + $db = \DBManager::get(); + $db->exec('DROP TABLE IF EXISTS `cw_clipboards`'); + } +} diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index bef6327d19c57e4acbd45a3e251ec2657bbfced4..f60c4dd5d1577bf3b13567c6ad4440e50ff07049 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -491,6 +491,16 @@ class RouteMap $group->delete('/courseware-units/{id}', Routes\Courseware\UnitsDelete::class); // not a JSON route $group->post('/courseware-units/{id}/copy', Routes\Courseware\UnitsCopy::class); + + $group->get('/courseware-clipboards', Routes\Courseware\ClipboardsIndex::class); + $group->get('/users/{id}/courseware-clipboards', Routes\Courseware\UsersClipboardsIndex::class); + $group->delete('/users/{id}/courseware-clipboards/{type:courseware-blocks|courseware-containers}', Routes\Courseware\UsersClipboardsDelete::class); + $group->get('/courseware-clipboards/{id}', Routes\Courseware\ClipboardsShow::class); + $group->post('/courseware-clipboards', Routes\Courseware\ClipboardsCreate::class); + $group->patch('/courseware-clipboards/{id}', Routes\Courseware\ClipboardsUpdate::class); + $group->delete('/courseware-clipboards/{id}', Routes\Courseware\ClipboardsDelete::class); + + $group->post('/courseware-clipboards/{id}/insert', Routes\Courseware\ClipboardsInsert::class); } private function addAuthenticatedFilesRoutes(RouteCollectorProxy $group): void diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php index 740364b6a1340963732803ec616a75498ed3b322..9fb281005b52d450b50aab46c963b85f55964b80 100644 --- a/lib/classes/JsonApi/Routes/Courseware/Authority.php +++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php @@ -5,6 +5,7 @@ namespace JsonApi\Routes\Courseware; use Courseware\Block; use Courseware\BlockComment; use Courseware\BlockFeedback; +use Courseware\Clipboard; use Courseware\Container; use Courseware\Instance; use Courseware\StructuralElement; @@ -512,4 +513,41 @@ class Authority return $request_user->id === $user->id; } + + public static function canShowClipboard(User $user, Clipboard $resource): bool + { + return $resource->user_id === $user->id; + } + + public static function canIndexClipboardsOfAUser(User $request_user, User $user): bool + { + return $request_user->id === $user->id; + } + + public static function canIndexClipboards(User $user): bool + { + return $GLOBALS['perm']->have_perm('root', $user->id); + } + + public static function canCreateClipboard(User $user): bool + { + return true; + } + + public static function canUpdateClipboard(User $user, Clipboard $resource): bool + { + return $resource->user_id === $user->id; + } + + public static function canDeleteClipboard(User $user, Clipboard $resource): bool + { + return self::canUpdateClipboard($user, $resource); + } + + public static function canDeleteClipboardsOfAUser(User $request_user, User $user): bool + { + return self::canIndexClipboardsOfAUser($request_user, $user); + } + + } diff --git a/lib/classes/JsonApi/Routes/Courseware/BlocksCopy.php b/lib/classes/JsonApi/Routes/Courseware/BlocksCopy.php index 3e34ed0e97dbd248955c422bdc09ac4d5b41afc3..d8bb1c5304f384e64a60f97f24f561e11cb08c17 100644 --- a/lib/classes/JsonApi/Routes/Courseware/BlocksCopy.php +++ b/lib/classes/JsonApi/Routes/Courseware/BlocksCopy.php @@ -29,14 +29,21 @@ class BlocksCopy extends NonJsonApiController $data = $request->getParsedBody()['data']; $block = \Courseware\Block::find($data['block']['id']); + if (!$block) { + throw new RecordNotFoundException(); + } $container = \Courseware\Container::find($data['parent_id']); + if (!$container) { + throw new RecordNotFoundException(); + } + $sectionIndex = $data['section']; $user = $this->getUser($request); if (!Authority::canCreateBlocks($user, $container) || !Authority::canUpdateBlock($user, $block)) { throw new AuthorizationFailedException(); } - $new_block = $this->copyBlock($user, $block, $container); + $new_block = $this->copyBlock($user, $block, $container, $sectionIndex); $response = $response->withHeader('Content-Type', 'application/json'); $response->getBody()->write((string) json_encode($new_block)); @@ -44,19 +51,8 @@ class BlocksCopy extends NonJsonApiController return $response; } - private function copyBlock(\User $user, \Courseware\Block $remote_block, \Courseware\Container $container) - { - - $block = $remote_block->copy($user, $container); - - $this->updateContainer($container, $block); - - return $block; - } - - private function updateContainer(\Courseware\Container $container, \Courseware\Block $block) + private function copyBlock(\User $user, \Courseware\Block $remote_block, \Courseware\Container $container, $sectionIndex) { - //TODO update section block ids - return true; + return $remote_block->copy($user, $container, $sectionIndex); } } diff --git a/lib/classes/JsonApi/Routes/Courseware/ClipboardsCreate.php b/lib/classes/JsonApi/Routes/Courseware/ClipboardsCreate.php new file mode 100644 index 0000000000000000000000000000000000000000..48aa78a464ffec6ad08410de180ffb12ec667bdd --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/ClipboardsCreate.php @@ -0,0 +1,123 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; +use JsonApi\Schemas\Courseware\Clipboard as ClipboardSchema; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Create a clipboard. + * + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.4 + */ +class ClipboardsCreate extends JsonApiController +{ + use ValidationTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $json = $this->validate($request); + $user = $this->getUser($request); + if (!Authority::canCreateClipboard($user)) { + throw new AuthorizationFailedException(); + } + $object = $this->getObject($json); + if (!$object) { + throw new RecordNotFoundException(); + } + $clipboard = $this->createClipboard($user, $json, $object); + + return $this->getCreatedResponse($clipboard); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (!self::arrayHas($json, 'data.attributes.name')) { + return 'Missing `name` value.'; + } + if (!self::arrayHas($json, 'data.attributes.object-type')) { + return 'Missing `object-type` value.'; + } + if ( + !( + self::arrayHas($json, 'data.attributes.block-id') + || self::arrayHas($json, 'data.attributes.container-id') + || self::arrayHas($json, 'data.attributes.structural-element-id') + ) + ) { + return 'Missing `block-id`, `container-id` or `structural-element-id` value.'; + } + if (!self::arrayHas($json, 'data.attributes.object-kind')) { + return 'Missing `object-kind` value.'; + } + if (!$this->validateObjectType($json)) { + return 'Invalid `object-type` value.'; + } + } + + private function createClipboard(\User $user, array $json, $object) + { + $clipboard = \Courseware\Clipboard::create([ + 'user_id' => $user->id, + 'name' => self::arrayGet($json, 'data.attributes.name'), + 'block_id' => self::arrayGet($json, 'data.attributes.block-id'), + 'container_id' => self::arrayGet($json, 'data.attributes.container-id'), + 'structural_element_id' => self::arrayGet($json, 'data.attributes.structural-element-id'), + 'object_type' => self::arrayGet($json, 'data.attributes.object-type'), + 'object_kind' => self::arrayGet($json, 'data.attributes.object-kind'), + 'backup' => $this->createBackup($object) + ]); + + return $clipboard; + } + + private function createBackup($object): string + { + return $object->getClipboardBackup(); + } + + private function validateObjectType($json): bool + { + $type = self::arrayGet($json, 'data.attributes.object-type'); + + return in_array($type, ['courseware-structural-elements', 'courseware-containers', 'courseware-blocks']); + } + + private function getObject($json): ?object + { + $object = null; + $type = self::arrayGet($json, 'data.attributes.object-type'); + + switch ($type) { + case 'courseware-structural-elements': + $object = \Courseware\StructuralElement::find(self::arrayGet($json, 'data.attributes.structural-element-id')); + break; + case 'courseware-containers': + $object = \Courseware\Container::find(self::arrayGet($json, 'data.attributes.container-id')); + break; + case 'courseware-blocks': + $object = \Courseware\Block::find(self::arrayGet($json, 'data.attributes.block-id')); + break; + } + + return $object; + } +} + diff --git a/lib/classes/JsonApi/Routes/Courseware/ClipboardsDelete.php b/lib/classes/JsonApi/Routes/Courseware/ClipboardsDelete.php new file mode 100644 index 0000000000000000000000000000000000000000..a917ec28d9ea785b8f91bde478ff3b9b90677b56 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/ClipboardsDelete.php @@ -0,0 +1,39 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Clipboard; +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; + +/** + * Delete one clipboard. + * + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.4 + */ +class ClipboardsDelete extends JsonApiController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $resource = Clipboard::find($args['id']); + if (!$resource) { + throw new RecordNotFoundException(); + } + $user = $this->getUser($request); + if (!Authority::canDeleteClipboard($user, $resource)) { + throw new AuthorizationFailedException(); + } + $resource->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/ClipboardsIndex.php b/lib/classes/JsonApi/Routes/Courseware/ClipboardsIndex.php new file mode 100644 index 0000000000000000000000000000000000000000..b3a5423904634bfea30f963ba8b31bfdf964a50e --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/ClipboardsIndex.php @@ -0,0 +1,42 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Clipboard; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\JsonApiController; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Displays all clipboards + * + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.4 + */ +class ClipboardsIndex extends JsonApiController +{ + protected $allowedPagingParameters = ['offset', 'limit']; + + protected $allowedIncludePaths = ['user']; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $user = $this->getUser($request); + if (!Authority::canIndexClipboards($user)) { + throw new AuthorizationFailedException(); + } + + list($offset, $limit) = $this->getOffsetAndLimit(); + + $total = Clipboard::countBySQL('1'); + $resources = Clipboard::findBySQL("1 ORDER BY mkdate LIMIT {$offset}, {$limit}"); + + return $this->getPaginatedContentResponse($resources, $total); + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Courseware/ClipboardsInsert.php b/lib/classes/JsonApi/Routes/Courseware/ClipboardsInsert.php new file mode 100644 index 0000000000000000000000000000000000000000..5f5f7d07905b6022f25025c23050871f8b196b8a --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/ClipboardsInsert.php @@ -0,0 +1,68 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use JsonApi\NonJsonApiController; +use Courseware\Block; +use Courseware\Clipboard; +use Courseware\Container; +use Courseware\StructuralElement; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\BadRequestException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\Errors\UnprocessableEntityException; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * + * + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.4 + */ + +class ClipboardsInsert extends NonJsonApiController +{ + public function __invoke(Request $request, Response $response, array $args) + { + $data = $request->getParsedBody()['data']; + $clipboard = Clipboard::find($args['id']); + + if (!$clipboard) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + $backup = json_decode($clipboard->backup); + + if ($clipboard->object_type === 'courseware-blocks') { + $sectionIndex = $data['section']; + $container = \Courseware\Container::find($data['parent_id']); + if (!$container) { + throw new RecordNotFoundException(); + } + if (!Authority::canCreateBlocks($user, $container)) { + throw new AuthorizationFailedException(); + } + $object = Block::createFromData($user, $backup, $container, $sectionIndex); + } + + if ($clipboard->object_type === 'courseware-containers') { + $element = \Courseware\StructuralElement::find($data['parent_id']); + if (!$element) { + throw new RecordNotFoundException(); + } + if (!Authority::canCreateContainer($user, $element)) { + throw new AuthorizationFailedException(); + } + $object = Container::createFromData($user, $backup, $element); + } + + $response = $response->withHeader('Content-Type', 'application/json'); + $response->getBody()->write((string) json_encode($object)); + + return $response; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/ClipboardsShow.php b/lib/classes/JsonApi/Routes/Courseware/ClipboardsShow.php new file mode 100644 index 0000000000000000000000000000000000000000..db1636769aa879b674f2bcad95f16fca37302326 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/ClipboardsShow.php @@ -0,0 +1,46 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Clipboard; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\Schemas\Courseware\Clipboard as ClipboardSchema; +use JsonApi\JsonApiController; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Displays one clipboard. + * + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.4 + */ +class ClipboardsShow extends JsonApiController +{ + protected $allowedIncludePaths = [ + ClipboardSchema::REL_USER, + ]; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param array $args + * @return Response + */ + public function __invoke(Request $request, Response $response, $args) + { + /** @var ?\Courseware\Clipboard $resource */ + $resource = Clipboard::find($args['id']); + if (!$resource) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowClipboard($this->getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($resource); + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Courseware/ClipboardsUpdate.php b/lib/classes/JsonApi/Routes/Courseware/ClipboardsUpdate.php new file mode 100644 index 0000000000000000000000000000000000000000..d9c8c952f225af23404b977007cedc095792a29d --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/ClipboardsUpdate.php @@ -0,0 +1,68 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Clipboard; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; +use JsonApi\Schemas\Courseware\Unit as UnitSchema; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Update one Clipboard. + * + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.4 + */ +class ClipboardsUpdate extends JsonApiController +{ + use ValidationTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $resource = Clipboard::find($args['id']); + if (!$resource) { + throw new RecordNotFoundException(); + } + $json = $this->validate($request, $resource); + $user = $this->getUser($request); + if (!Authority::canUpdateClipboard($user, $resource)) { + throw new AuthorizationFailedException(); + } + $resource = $this->updateClipboard($user, $resource, $json); + + return $this->getContentResponse($resource); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + protected function validateResourceDocument($json, $resource) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + } + + private function updateClipboard(\User $user, Clipboard $resource, array $json): Clipboard + { + if (self::arrayHas($json, 'data.attributes.name')) { + $resource->name = self::arrayGet($json, 'data.attributes.name'); + } + if (self::arrayHas($json, 'data.attributes.description')) { + $resource->description = self::arrayGet($json, 'data.attributes.description'); + } + + $resource->store(); + + return $resource; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/ContainersCopy.php b/lib/classes/JsonApi/Routes/Courseware/ContainersCopy.php index cbef54c762e239b10d792d567390e1fcc3481427..eea3d5aa10df13d9cda4dca38afd159af774856f 100644 --- a/lib/classes/JsonApi/Routes/Courseware/ContainersCopy.php +++ b/lib/classes/JsonApi/Routes/Courseware/ContainersCopy.php @@ -29,7 +29,13 @@ class ContainersCopy extends NonJsonApiController $data = $request->getParsedBody()['data']; $container = \Courseware\Container::find($data['container']['id']); + if (!$container) { + throw new RecordNotFoundException(); + } $element = \Courseware\StructuralElement::find($data['parent_id']); + if (!$element) { + throw new RecordNotFoundException(); + } $user = $this->getUser($request); if (!Authority::canCreateContainer($user, $element) || !Authority::canUpdateContainer($user, $container)) { throw new AuthorizationFailedException(); diff --git a/lib/classes/JsonApi/Routes/Courseware/UsersClipboardsDelete.php b/lib/classes/JsonApi/Routes/Courseware/UsersClipboardsDelete.php new file mode 100644 index 0000000000000000000000000000000000000000..38694f2fca0ba2d70fae694cfc2a717aaf97debb --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/UsersClipboardsDelete.php @@ -0,0 +1,40 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Clipboard; +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; + +/** + * Displays all clipboards of one user. + * + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.4 + */ +class UsersClipboardsDelete extends JsonApiController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $user = \User::find($args['id']); + if (!$user) { + throw new RecordNotFoundException(); + } + $request_user = $this->getUser($request); + if (!Authority::canDeleteClipboardsOfAUser($request_user, $user)) { + throw new AuthorizationFailedException(); + } + + Clipboard::deleteUsersClipboards($user, $args['type']); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/UsersClipboardsIndex.php b/lib/classes/JsonApi/Routes/Courseware/UsersClipboardsIndex.php new file mode 100644 index 0000000000000000000000000000000000000000..6b98d01331e9befddb0c810dea5a5391343f016c --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/UsersClipboardsIndex.php @@ -0,0 +1,50 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Clipboard; +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; + +/** + * Displays all clipboards of one user. + * + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.4 + */ +class UsersClipboardsIndex extends JsonApiController +{ + use CoursewareInstancesHelper; + + protected $allowedIncludePaths = [ + 'user', + ]; + + protected $allowedPagingParameters = ['offset', 'limit']; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $user = \User::find($args['id']); + if (!$user) { + throw new RecordNotFoundException(); + } + $request_user = $this->getUser($request); + if (!Authority::canIndexClipboardsOfAUser($request_user, $user)) { + throw new AuthorizationFailedException(); + } + + $resources = Clipboard::findUsersClipboards($user); + $total = count($resources); + [$offset, $limit] = $this->getOffsetAndLimit(); + + return $this->getPaginatedContentResponse(array_slice($resources, $offset, $limit), $total); + } +} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index 44b12d9639bf0445f815a89c0990b7333995e724..19d21f5648dae951cdbd0edf3b586724b672b0f4 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -54,6 +54,7 @@ class SchemaMap \Courseware\Block::class => Schemas\Courseware\Block::class, \Courseware\BlockComment::class => Schemas\Courseware\BlockComment::class, \Courseware\BlockFeedback::class => Schemas\Courseware\BlockFeedback::class, + \Courseware\Clipboard::class => Schemas\Courseware\Clipboard::class, \Courseware\Container::class => Schemas\Courseware\Container::class, \Courseware\Instance::class => Schemas\Courseware\Instance::class, \Courseware\StructuralElement::class => Schemas\Courseware\StructuralElement::class, diff --git a/lib/classes/JsonApi/Schemas/Courseware/Clipboard.php b/lib/classes/JsonApi/Schemas/Courseware/Clipboard.php new file mode 100644 index 0000000000000000000000000000000000000000..e65f1b108be5a9ce74b8330e462b6730ea1293cd --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Courseware/Clipboard.php @@ -0,0 +1,90 @@ +<?php + +namespace JsonApi\Schemas\Courseware; + +use JsonApi\Schemas\SchemaProvider; +use Neomerx\JsonApi\Contracts\Schema\ContextInterface; +use Neomerx\JsonApi\Schema\Link; + +class Clipboard extends SchemaProvider +{ + const TYPE = 'courseware-clipboards'; + + const REL_USER = 'user'; + const REL_STRUCTURAL_ELEMENT = 'structural-element'; + const REL_CONTAINER = 'container'; + const REL_BLOCK = 'block'; + + /** + * {@inheritdoc} + */ + public function getId($resource): ?string + { + return $resource->id; + } + + /** + * {@inheritdoc} + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'name' => (string) $resource->name, + 'description' => (string) $resource->description, + 'block-id' => (int) $resource->block_id, + 'container-id' => (int) $resource->container_id, + 'structural-element-id' => (int) $resource->structural_element_id, + 'object-type' => (string) $resource->object_type, + 'object-kind' => (string) $resource->object_kind, + 'backup' => $resource->backup, + 'mkdate' => date('c', $resource->mkdate), + 'chdate' => date('c', $resource->chdate), + ]; + } + + /** + * {@inheritdoc} + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $relationships = []; + + $relationships[self::REL_USER] = $resource->user + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->user), + ], + self::RELATIONSHIP_DATA => $resource->user, + ] + : [self::RELATIONSHIP_DATA => null]; + + $relationships[self::REL_BLOCK] = $resource->block + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->block), + ], + self::RELATIONSHIP_DATA => $resource->block, + ] + : [self::RELATIONSHIP_DATA => null]; + + $relationships[self::REL_CONTAINER] = $resource->container + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->container), + ], + self::RELATIONSHIP_DATA => $resource->container, + ] + : [self::RELATIONSHIP_DATA => null]; + + $relationships[self::REL_STRUCTURAL_ELEMENT] = $resource->structural_element + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->structural_element), + ], + self::RELATIONSHIP_DATA => $resource->structural_element, + ] + : [self::RELATIONSHIP_DATA => null]; + + return $relationships; + } +} \ No newline at end of file diff --git a/lib/models/Courseware/Block.php b/lib/models/Courseware/Block.php index bd0361897dd65c93f11a903f1dc86d032f93f581..d311e0fc4911429bc8ff310f2b96451f929ef784 100644 --- a/lib/models/Courseware/Block.php +++ b/lib/models/Courseware/Block.php @@ -162,6 +162,25 @@ class Block extends \SimpleORMap implements \PrivacyObject return $user->getFullName(); } + public function getClipboardBackup(): string + { + $block = [ + 'type' => 'courseware-blocks', + 'id' => $this->id, + 'attributes' => [ + 'position'=> $this->position, + 'block-type'=> $this->type->getType(), + 'title'=> $this->type->getTitle(), + 'visible'=> $this->visible, + 'payload'=> $this->type->getPayload(), + 'mkdate'=> $this->mkdate, + 'chdate'=> $this->chdate + ] + ]; + + return json_encode($block, true); + } + /** * Copies this block into another container such that the given user is the owner of the copy. * @@ -170,13 +189,13 @@ class Block extends \SimpleORMap implements \PrivacyObject * * @return Block the copy of this block */ - public function copy(User $user, Container $container): Block + public function copy(User $user, Container $container, $sectionIndex = null): Block { /** @var StructuralElement $struct */ - $struct = StructuralElement::find($container->structural_element_id); + $struct = $container->structural_element; $rangeId = $struct->getRangeId(); - $block = self::build([ + $block = self::create([ 'container_id' => $container->id, 'owner_id' => $user->id, 'editor_id' => $user->id, @@ -187,10 +206,34 @@ class Block extends \SimpleORMap implements \PrivacyObject 'visible' => 1, ]); + //update Container payload + $container->type->addBlock($block, $sectionIndex); + $container->store(); + + return $block; + } + + public static function createFromData(User $user, $data, Container $container, $sectionIndex = null): Block + { + $struct = $container->structural_element; + $rangeId = $struct->getRangeId(); + + $block = self::create([ + 'container_id' => $container->id, + 'owner_id' => $user->id, + 'editor_id' => $user->id, + 'edit_blocker_id' => null, + 'position' => $container->countBlocks(), + 'block_type' => $data->attributes->{'block-type'}, + 'visible' => 1, + ]); + + $dataPayload = (array)$data->attributes->payload; + $block->payload = json_encode($block->type->copyPayload('', $dataPayload), true); $block->store(); //update Container payload - $container->type->addBlock($block); + $container->type->addBlock($block, $sectionIndex); $container->store(); return $block; diff --git a/lib/models/Courseware/BlockTypes/Audio.php b/lib/models/Courseware/BlockTypes/Audio.php index dd2588a1e5ea301f8a5697591528790a2ba24435..7196f209a7449b6a17a0f56f79094344ad9e96b2 100644 --- a/lib/models/Courseware/BlockTypes/Audio.php +++ b/lib/models/Courseware/BlockTypes/Audio.php @@ -74,11 +74,13 @@ class Audio extends BlockType } } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } - if ('' != $payload['file_id']) { + if (!empty($payload['file_id'])) { $payload['file_id'] = $this->copyFileById($payload['file_id'], $rangeId); } diff --git a/lib/models/Courseware/BlockTypes/BeforeAfter.php b/lib/models/Courseware/BlockTypes/BeforeAfter.php index b4f33ca9c0854dbefc6eea994887f764547330c3..5ad772cac72be9b2548950fcfffc5495222a317e 100644 --- a/lib/models/Courseware/BlockTypes/BeforeAfter.php +++ b/lib/models/Courseware/BlockTypes/BeforeAfter.php @@ -75,11 +75,13 @@ class BeforeAfter extends BlockType return $files; } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } - if ('' != $payload['before_file_id']) { + if (!empty($payload['before_file_id'])) { $payload['before_file_id'] = $this->copyFileById($payload['before_file_id'], $rangeId); } diff --git a/lib/models/Courseware/BlockTypes/BlockType.php b/lib/models/Courseware/BlockTypes/BlockType.php index 5c21c01bb983036fc5d59014a1c91d4de648590d..8c03c8808326022e82075a28fe89dee2c566814d 100644 --- a/lib/models/Courseware/BlockTypes/BlockType.php +++ b/lib/models/Courseware/BlockTypes/BlockType.php @@ -232,9 +232,9 @@ abstract class BlockType } // TODO: (tgloeggl) DocBlock ergänzen - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - return $this->getPayload(); + return $payload ?: $this->getPayload(); } /** @@ -339,6 +339,10 @@ abstract class BlockType return $file_map[$fileId]; } + if ($rangeId === '') { + $rangeId = $this->block->container->structural_element->range_id; + } + $user = \User::findCurrent(); if ($file_ref = \FileRef::find($fileId)) { $copiedFile = \FileManager::copyFile( @@ -375,6 +379,10 @@ abstract class BlockType return $folder_map[$folderId]; } + if ($rangeId === '') { + $rangeId = $this->block->container->structural_element->range_id; + } + $user = \User::findCurrent(); $destinationFolder = $this->getDestinationFolder($user, $rangeId); if ($sourceFolder = \Folder::find($folderId)) { diff --git a/lib/models/Courseware/BlockTypes/Canvas.php b/lib/models/Courseware/BlockTypes/Canvas.php index e7b14e99665bd9ed3745aae213e7d8b41d2ff82e..34dc431b5b0f9e123f60f1571ea9f3b6e0f820ad 100644 --- a/lib/models/Courseware/BlockTypes/Canvas.php +++ b/lib/models/Courseware/BlockTypes/Canvas.php @@ -65,11 +65,13 @@ class Canvas extends BlockType return $files; } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } - if ('' != $payload['file_id']) { + if (!empty($payload['file_id'])) { $payload['file_id'] = $this->copyFileById($payload['file_id'], $rangeId); } diff --git a/lib/models/Courseware/BlockTypes/DialogCards.php b/lib/models/Courseware/BlockTypes/DialogCards.php index 74b843cbe268886314886aa4ccae4e1514b6ad8f..62eea5fb3f99857b1d49fdf603b5d50abcd366f9 100644 --- a/lib/models/Courseware/BlockTypes/DialogCards.php +++ b/lib/models/Courseware/BlockTypes/DialogCards.php @@ -80,9 +80,12 @@ class DialogCards extends BlockType return $files; } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } + foreach ($payload['cards'] as &$card) { if ('' != $card['front_file_id']) { $card['front_file_id'] = $this->copyFileById($card['front_file_id'], $rangeId); diff --git a/lib/models/Courseware/BlockTypes/Document.php b/lib/models/Courseware/BlockTypes/Document.php index db1ba6ee179fb0dfcc6ccc4d6808a2a155cb8eef..20950089c5bca208e4a8c7a361e17dde305e27da 100644 --- a/lib/models/Courseware/BlockTypes/Document.php +++ b/lib/models/Courseware/BlockTypes/Document.php @@ -67,11 +67,13 @@ class Document extends BlockType return $files; } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } - if ('' != $payload['file_id']) { + if (!empty($payload['file_id'])) { $payload['file_id'] = $this->copyFileById($payload['file_id'], $rangeId); } diff --git a/lib/models/Courseware/BlockTypes/Download.php b/lib/models/Courseware/BlockTypes/Download.php index d736009d1815230ec4a52367b32a8ff756039adb..f53c8c7f288331552eeb52de204701c16fa17bfc 100644 --- a/lib/models/Courseware/BlockTypes/Download.php +++ b/lib/models/Courseware/BlockTypes/Download.php @@ -61,11 +61,13 @@ class Download extends BlockType return $files; } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } - if ('' != $payload['file_id']) { + if (!empty($payload['file_id'])) { $payload['file_id'] = $this->copyFileById($payload['file_id'], $rangeId); } diff --git a/lib/models/Courseware/BlockTypes/Folder.php b/lib/models/Courseware/BlockTypes/Folder.php index bc34f854f24fc547829deeed13a9e04058dca5c8..d5156b6298d04d63883d9acd3833320213477b07 100644 --- a/lib/models/Courseware/BlockTypes/Folder.php +++ b/lib/models/Courseware/BlockTypes/Folder.php @@ -104,11 +104,13 @@ class Folder extends BlockType return \FileRef::findByFolder_id($payload['folder_id']); } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } - if ('' != $payload['folder_id']) { + if (!empty($payload['folder_id'])) { $payload['folder_id'] = $this->copyFolderById($payload['folder_id'], $rangeId); } diff --git a/lib/models/Courseware/BlockTypes/Gallery.php b/lib/models/Courseware/BlockTypes/Gallery.php index 5f9bb0b6d56d3c8c8d5acc5f503f09306cef3f8d..1d931ea99519104d24a1a958f773bf6c44855533 100644 --- a/lib/models/Courseware/BlockTypes/Gallery.php +++ b/lib/models/Courseware/BlockTypes/Gallery.php @@ -123,10 +123,13 @@ class Gallery extends BlockType return $files; } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); - if ('' != $payload['folder_id']) { + if (!$payload) { + $payload = $this->getPayload(); + } + + if (!empty($payload['folder_id'])) { $payload['folder_id'] = $this->copyFolderById($payload['folder_id'], $rangeId); } diff --git a/lib/models/Courseware/BlockTypes/Headline.php b/lib/models/Courseware/BlockTypes/Headline.php index 0c821386b82dee191489486bb10b5e2213596bc6..eace9465685f2a23c57fd76dcc745d23c4aea9f2 100644 --- a/lib/models/Courseware/BlockTypes/Headline.php +++ b/lib/models/Courseware/BlockTypes/Headline.php @@ -73,11 +73,13 @@ class Headline extends BlockType return $files; } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } - if ('' != $payload['background_image_id']) { + if (!empty($payload['background_image_id'])) { $payload['background_image_id'] = $this->copyFileById($payload['background_image_id'], $rangeId); $payload['background_image'] = ''; } diff --git a/lib/models/Courseware/BlockTypes/ImageMap.php b/lib/models/Courseware/BlockTypes/ImageMap.php index e275c46254ef90dd66b8e008977e41b1de9f8aa1..3f8f37b2b61c346c2b3ba078c8624483d132e47c 100644 --- a/lib/models/Courseware/BlockTypes/ImageMap.php +++ b/lib/models/Courseware/BlockTypes/ImageMap.php @@ -55,11 +55,13 @@ class ImageMap extends BlockType return $files; } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } - if ('' != $payload['file_id']) { + if (!empty($payload['file_id'])) { $payload['file_id'] = $this->copyFileById($payload['file_id'], $rangeId); } diff --git a/lib/models/Courseware/BlockTypes/Text.php b/lib/models/Courseware/BlockTypes/Text.php index 0a8921932353c36ada25a896e5358cf791594a5c..e918c6a44e5ae1e68707adbfd732559a2c2f794e 100644 --- a/lib/models/Courseware/BlockTypes/Text.php +++ b/lib/models/Courseware/BlockTypes/Text.php @@ -88,9 +88,11 @@ class Text extends BlockType return $files; } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } $document = new \DOMDocument(); if ($payload['text']) { diff --git a/lib/models/Courseware/BlockTypes/Video.php b/lib/models/Courseware/BlockTypes/Video.php index 431f69b68e6db77e87980ee7716cfcd03bd67345..a32ca668f43dcbe7d4a8115e40bd407bad7099a7 100644 --- a/lib/models/Courseware/BlockTypes/Video.php +++ b/lib/models/Courseware/BlockTypes/Video.php @@ -68,11 +68,13 @@ class Video extends BlockType } - public function copyPayload(string $rangeId = ''): array + public function copyPayload(string $rangeId = '', $payload = null): array { - $payload = $this->getPayload(); + if (!$payload) { + $payload = $this->getPayload(); + } - if ('' != $payload['file_id']) { + if (!empty($payload['file_id'])) { $payload['file_id'] = $this->copyFileById($payload['file_id'], $rangeId); } diff --git a/lib/models/Courseware/Clipboard.php b/lib/models/Courseware/Clipboard.php new file mode 100644 index 0000000000000000000000000000000000000000..7a42d33bf935589a4f388c89758968aeb6b0eb85 --- /dev/null +++ b/lib/models/Courseware/Clipboard.php @@ -0,0 +1,78 @@ +<?php + +namespace Courseware; + +use User; + +/** + * Courseware's clipboards. + * + * @author Ron Lucke <lucke@elan-ev.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.4 + * + * @property int $id database column + * @property string $user_id database column + * @property string $name database column + * @property string $description database column + * @property int $block_id database column + * @property int $container_id database column + * @property int $structural_element_id database column + * @property string $object_type database column + * @property string $object_kind database column + * @property string $backup database column + * @property int $mkdate database column + * @property int $chdate database column + * @property \User $user belongs_to User + * @property Block $block belongs_to Block + * @property Container $container belongs_to Container + * @property StructuralElement $structural_element belongs_to StructuralElement + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ + +class Clipboard extends \SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'cw_clipboards'; + + $config['belongs_to']['user'] = [ + 'class_name' => User::class, + 'foreign_key' => 'user_id', + 'on_delete' => 'delete', + ]; + + $config['belongs_to']['block'] = [ + 'class_name' => Block::class, + 'foreign_key' => 'block_id', + ]; + + $config['belongs_to']['container'] = [ + 'class_name' => Container::class, + 'foreign_key' => 'container_id', + ]; + + $config['belongs_to']['structural_element'] = [ + 'class_name' => StructuralElement::class, + 'foreign_key' => 'structural_element_id', + ]; + + parent::configure($config); + } + + public static function findUsersClipboards($user): array + { + return self::findBySQL('user_id = ?', [$user->id]); + } + + public static function deleteUsersClipboards($user, $type): void + { + self::deleteBySQL( + 'user_id = ? AND object_type = ?', + [$user->id, $type] + ); + } + +} diff --git a/lib/models/Courseware/Container.php b/lib/models/Courseware/Container.php index 082a9a0176904f136b2ef46c17180b190e11e9ad..5ce8f29a7d96244930edebf7b4377f58d7ab12a7 100644 --- a/lib/models/Courseware/Container.php +++ b/lib/models/Courseware/Container.php @@ -98,6 +98,33 @@ class Container extends \SimpleORMap implements \PrivacyObject return Block::countBySql('container_id = ?', [$this->id]); } + public function getClipboardBackup(): string + { + $container = [ + 'type' => 'courseware-containers', + 'id' => $this->id, + 'attributes' => [ + 'position' => $this->position, + 'site' => $this->site, + 'container-type' => $this->type->getType(), + 'title' => $this->type->getTitle(), + 'visible' => $this->visible, + 'payload' => $this->type->getPayload(), + 'mkdate' => $this->mkdate, + 'chdate' => $this->chdate + ], + 'blocks' => $this->getClipboardBackupBlocks() + ]; + return json_encode($container, true); + } + + public function getClipboardBackupBlocks(): array + { + return $this->blocks->map(function (Block $block) { + return json_decode($block->getClipboardBackup()); + }); + } + /** * Copies this block into another structural element such that the given user is the owner of the copy. * @@ -108,7 +135,7 @@ class Container extends \SimpleORMap implements \PrivacyObject */ public function copy(User $user, StructuralElement $element): array { - $container = self::build([ + $container = self::create([ 'structural_element_id' => $element->id, 'owner_id' => $user->id, 'editor_id' => $user->id, @@ -118,8 +145,6 @@ class Container extends \SimpleORMap implements \PrivacyObject 'payload' => $this['payload'], ]); - $container->store(); - list($blockMapIds, $blockMapObjs) = $this->copyBlocks($user, $container); $container['payload'] = $container->type->copyPayload($blockMapIds); @@ -160,4 +185,35 @@ class Container extends \SimpleORMap implements \PrivacyObject } } + + public static function createFromData(User $user, $data, StructuralElement $element): Container + { + $container = self::create([ + 'structural_element_id' => $element->id, + 'owner_id' => $user->id, + 'editor_id' => $user->id, + 'edit_blocker_id' => null, + 'position' => $element->countContainers(), + 'container_type' => $data->attributes->{'container-type'}, + 'payload' => json_encode($data->attributes->payload), + ]); + + $blockMap = self::createBlocksFromData($user, $container, $data); + $container['payload'] = $container->type->copyPayload($blockMap); + $container->store(); + + return $container; + } + + private static function createBlocksFromData($user, $container, $data): array + { + $blockMap = []; + + foreach ($data->blocks as $block) { + $newBlock = Block::createFromData($user, $block, $container); + $blockMap[$block->id] = $newBlock->id; + } + + return $blockMap; + } } diff --git a/lib/models/Courseware/ContainerTypes/AccordionContainer.php b/lib/models/Courseware/ContainerTypes/AccordionContainer.php index 832520401aa807626f1db1e5df5ed5b09cf90f31..185b6d113d8b26eac1eb9dc8587ab79ac359f247 100644 --- a/lib/models/Courseware/ContainerTypes/AccordionContainer.php +++ b/lib/models/Courseware/ContainerTypes/AccordionContainer.php @@ -41,11 +41,15 @@ class AccordionContainer extends ContainerType ]; } - public function addBlock($block): void + public function addBlock($block, $sectionIndex = null): void { $payload = $this->getPayload(); - array_push($payload['sections'][count($payload['sections']) - 1]['blocks'], $block->id); + if ($sectionIndex !== null) { + array_push($payload['sections'][$sectionIndex]['blocks'], $block->id); + } else { + array_push($payload['sections'][count($payload['sections']) - 1]['blocks'], $block->id); + } $this->setPayload($payload); } diff --git a/lib/models/Courseware/ContainerTypes/ContainerType.php b/lib/models/Courseware/ContainerTypes/ContainerType.php index dbe6df255b546369fcc50fab1a3e1c0d230e91e3..6cbcdf964a138a9c58f1dcfc9595fb02cdee5bcd 100644 --- a/lib/models/Courseware/ContainerTypes/ContainerType.php +++ b/lib/models/Courseware/ContainerTypes/ContainerType.php @@ -58,7 +58,7 @@ abstract class ContainerType * * @param /Courseware/Block $block - block to be added to the container */ - abstract public function addBlock($block): void; + abstract public function addBlock($block, $sectionIndex): void; /** * Returns all known types of containers: core types and plugin types as well. @@ -197,8 +197,9 @@ abstract class ContainerType foreach ($payload['sections'] as &$section) { foreach ($section['blocks'] as &$block) { - $block = $block_map[$block] ?? null; + $block = strval($block_map[$block]) ?? null; } + $section['blocks'] = array_filter($section['blocks']); } return $payload; diff --git a/lib/models/Courseware/ContainerTypes/ListContainer.php b/lib/models/Courseware/ContainerTypes/ListContainer.php index 4918271a40e322e5aafb03748adc276159d49570..b98d8a1aaf9aa57c7cc60e54b19bba43016bf54b 100644 --- a/lib/models/Courseware/ContainerTypes/ListContainer.php +++ b/lib/models/Courseware/ContainerTypes/ListContainer.php @@ -41,7 +41,7 @@ class ListContainer extends ContainerType ]; } - public function addBlock($block): void + public function addBlock($block, $sectionIndex = null): void { $payload = $this->getPayload(); diff --git a/lib/models/Courseware/ContainerTypes/TabsContainer.php b/lib/models/Courseware/ContainerTypes/TabsContainer.php index b884bbb67ee0a745137d01d80cbf3495b641721c..01f3d451043a3d79148745bfaee3a0a9a566a436 100644 --- a/lib/models/Courseware/ContainerTypes/TabsContainer.php +++ b/lib/models/Courseware/ContainerTypes/TabsContainer.php @@ -42,11 +42,14 @@ class TabsContainer extends ContainerType ]; } - public function addBlock($block): void + public function addBlock($block, $sectionIndex = null): void { $payload = $this->getPayload(); - - array_push($payload['sections'][count($payload['sections']) - 1]['blocks'], $block->id); + if ($sectionIndex !== null) { + array_push($payload['sections'][$sectionIndex]['blocks'], $block->id); + } else { + array_push($payload['sections'][count($payload['sections']) - 1]['blocks'], $block->id); + } $this->setPayload($payload); } diff --git a/lib/models/Courseware/StructuralElement.php b/lib/models/Courseware/StructuralElement.php index fabfd8465e6638751ae88ff7381767a0e6f2c340..a4f5cf1f8f0e4ef8fea8f6605a15751e1ef07796 100644 --- a/lib/models/Courseware/StructuralElement.php +++ b/lib/models/Courseware/StructuralElement.php @@ -769,6 +769,12 @@ SQL; return $this->image ? $this->image->getDownloadURL() : null; } + public static function getClipboardBackup(): string + { + //TODO + return ''; + } + /** * Copies this instance into another course oder users contents. * diff --git a/resources/assets/javascripts/lib/actionmenu.js b/resources/assets/javascripts/lib/actionmenu.js index 8acf80cfa3207c8df679f1e82ff95848ad4b88ba..42e5e783aa1319c256da4d1e8b3218db2f2c2954 100644 --- a/resources/assets/javascripts/lib/actionmenu.js +++ b/resources/assets/javascripts/lib/actionmenu.js @@ -117,9 +117,10 @@ class ActionMenu { this.content = $('.action-menu-content', element); this.is_reversed = reversed; this.is_open = false; + const additionalClasses = Object.values({ ...this.element[0].classList }).filter((item) => item != 'action-menu'); const menu_width = this.content.width(); const menu_height = this.content.height(); - + // Reposition the menu? if (position) { const form = this.element.closest('form'); @@ -135,6 +136,7 @@ class ActionMenu { $('.action-menu-icon', element).clone().data('action-menu-element', element).prependTo(this.menu); this.menu + .addClass(additionalClasses.join(' ')) .offset(this.element.offset()) .appendTo(breakpoint); diff --git a/resources/assets/stylesheets/scss/buttons.scss b/resources/assets/stylesheets/scss/buttons.scss index ef819817c0e207401fc898f5deb53cc6446368a9..659a26a0a5b4ef7bb2b4ad64f1205978f4dcdc16 100644 --- a/resources/assets/stylesheets/scss/buttons.scss +++ b/resources/assets/stylesheets/scss/buttons.scss @@ -109,6 +109,9 @@ button.button { .button.arr_left { @include button-with-icon(arr_1left, clickable, info_alt); } +.button.refresh { + @include button-with-icon(refresh, clickable, info_alt); +} .button.arr_right { @include button-with-icon(arr_1right, clickable, info_alt); &::before { diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index 964d1bf573477031e0c8eb0d52e4e000fe31753c..a4508514d0c9bb9e6a113a0e5d9e6ce25330a5cd 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -1920,7 +1920,6 @@ b l o c k a d d e r &:hover { border-color: var(--base-color); } - .cw-blockadder-item { padding: 64px 10px 4px 10px; @include background-icon(unit-test, clickable, 48); @@ -1933,7 +1932,7 @@ b l o c k a d d e r @include background-icon($icon, clickable, 48); } } - + .cw-clipboard-item-title, .cw-blockadder-item-title { display: inline-block; font-weight: 600; @@ -1954,7 +1953,6 @@ b l o c k a d d e r } } - .cw-block-adder-area { background-color: $white; border: solid thin $content-color-40; @@ -2074,6 +2072,63 @@ b l o c k a d d e r } } } +.cw-element-inserter-wrapper { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} + + +.cw-clipboard-item-wrapper { + display: flex; + width: calc(50% - 4px); + border: solid thin var(--content-color-40); + margin-bottom: 4px; + + &:hover { + border-color: var(--base-color); + } + + .cw-clipboard-item { + width: 207px; + padding: 64px 10px 4px 10px; + @include background-icon(unit-test, clickable, 48); + background-position: 10px 10px; + background-repeat: no-repeat; + cursor: pointer; + background-color: var(--white); + border: none; + text-align: left; + color: var(--base-color); + + @each $item, $icon in $blockadder-items { + &.cw-clipboard-item-#{$item} { + @include background-icon($icon, clickable, 48); + } + } + @each $item, $icon in $containeradder-items { + &.cw-clipboard-item-#{$item} { + @include background-icon($icon, clickable, 48); + } + } + + .cw-clipboard-item-title { + display: inline-block; + font-weight: 600; + margin-bottom: 2px; + } + + } + .cw-clipboard-item-action-menu-wrapper { + padding: 8px; + } +} +.action-menu.is-open, +.action-menu-wrapper.is-open { + &.cw-clipboard-item-action-menu { + z-index: 42; + } +} /* * * * * * * * * * * * * b l o c k a d d e r e n d diff --git a/resources/vue/components/courseware/CoursewareBlockActions.vue b/resources/vue/components/courseware/CoursewareBlockActions.vue index 031e3aa4d99cfbeed1c49cc0f38f569afa814e26..2b6252789e8539d662abccdc32badcad3eb2b89b 100644 --- a/resources/vue/components/courseware/CoursewareBlockActions.vue +++ b/resources/vue/components/courseware/CoursewareBlockActions.vue @@ -8,6 +8,7 @@ @showInfo="showInfo" @deleteBlock="deleteBlock" @removeLock="removeLock" + @copyToClipboard="copyToClipboard" /> </div> </template> @@ -53,7 +54,7 @@ export default { if (!this.blocked) { menuItems.push({ id: 1, label: this.$gettext('Block bearbeiten'), icon: 'edit', emit: 'editBlock' }); menuItems.push({ - id: 2, + id: 3, label: this.block.attributes.visible ? this.$gettext('unsichtbar setzen') : this.$gettext('sichtbar setzen'), @@ -77,6 +78,12 @@ export default { emit: 'deleteBlock' }); } + menuItems.push({ + id: 2, + label: this.$gettext('Block merken'), + icon: 'clipboard', + emit: 'copyToClipboard' + }); menuItems.push({ id: 7, label: this.$gettext('Informationen zum Block'), @@ -135,6 +142,9 @@ export default { }, removeLock() { this.$emit('removeLock'); + }, + copyToClipboard() { + this.$emit('copyToClipboard'); } }, }; diff --git a/resources/vue/components/courseware/CoursewareClipboardItem.vue b/resources/vue/components/courseware/CoursewareClipboardItem.vue new file mode 100644 index 0000000000000000000000000000000000000000..aa975c8b46ce33ac58f7211914756899b067f8aa --- /dev/null +++ b/resources/vue/components/courseware/CoursewareClipboardItem.vue @@ -0,0 +1,246 @@ +<template> + <div class="cw-clipboard-item-wrapper"> + <button class="cw-clipboard-item" :class="['cw-clipboard-item-' + kind]" @click.prevent="insertItem"> + <header class="sr-only"> + {{ srTitle }} + </header> + <header class="cw-clipboard-item-title" aria-hidden="true"> + {{ name }} + </header> + <p class="cw-clipboard-item-description"> + {{ description }} + </p> + </button> + <div class="cw-clipboard-item-action-menu-wrapper"> + <studip-action-menu + class="cw-clipboard-item-action-menu" + :items="menuItems" + :context="name" + @insertItemCopy="insertItemCopy" + @editItem="showEditItem" + @deleteItem="deleteItem" + /> + </div> + <studip-dialog + v-if="showEditDialog" + :title="$gettext('Umbenennen')" + :confirmText="$gettext('Speichern')" + confirmClass="accept" + :closeText="$gettext('Abbrechen')" + closeClass="cancel" + height="360" + width="500" + @close="closeEditItem" + @confirm="storeItem" + > + <template v-slot:dialogContent> + <form class="default" @submit.prevent=""> + <label> + {{ $gettext('Titel') }} + <input type="text" v-model="currentClipboard.attributes.name" /> + </label> + <label> + {{ $gettext('Beschreibung') }} + <textarea v-model="currentClipboard.attributes.description"></textarea> + </label> + </form> + </template> + </studip-dialog> + </div> +</template> + +<script> +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-clipboard-item', + components: {}, + props: { + clipboard: Object, + }, + data() { + return { + showEditDialog: false, + currentClipboard: null, + + text: { + errorMessage: this.$gettext('Es ist ein Fehler aufgetreten.'), + positionWarning: this.$gettext( + 'Bitte wählen Sie einen Ort aus, an dem der Block eingefügt werden soll.' + ), + blockSuccess: this.$gettext('Der Block wurde erfolgreich eingefügt.'), + containerSuccess: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'), + }, + }; + }, + computed: { + ...mapGetters({ + blockAdder: 'blockAdder', + currentElement: 'currentElement', + }), + name() { + return this.clipboard.attributes.name; + }, + description() { + return this.clipboard.attributes.description; + }, + isBlock() { + return this.clipboard.attributes['object-type'] === 'courseware-blocks'; + }, + kind() { + return this.clipboard.attributes['object-kind']; + }, + blockId() { + return this.clipboard.attributes['block-id']; + }, + blockNotFound() { + return this.clipboard.relationships.block.data === null; + }, + containerId() { + return this.clipboard.attributes['container-id']; + }, + containerNotFound() { + return this.clipboard.relationships.container.data === null; + }, + itemNotFound() { + if (this.isBlock) { + return this.blockNotFound; + } + + return this.containerNotFound; + }, + menuItems() { + let menuItems = []; + if (!this.itemNotFound) { + menuItems.push({ + id: 1, + label: this.$gettext('Kopie des aktuellen Stands einfügen'), + icon: 'copy', + emit: 'insertItemCopy', + }); + } + menuItems.push({ id: 2, label: this.$gettext('Umbenennen'), icon: 'edit', emit: 'editItem' }); + menuItems.push({ id: 3, label: this.$gettext('Löschen'), icon: 'trash', emit: 'deleteItem' }); + + menuItems.sort((a, b) => a.id - b.id); + return menuItems; + }, + blockAdderActive() { + return Object.keys(this.blockAdder).length !== 0; + }, + srTitle() { + return this.isBlock ? + this.$gettextInterpolate(this.$gettext(`Block %{name} einfügen`), { name: this.name }) : + this.$gettextInterpolate(this.$gettext(`Abschnitt %{name} einfügen`), { name: this.name }); + } + }, + methods: { + ...mapActions({ + companionInfo: 'companionInfo', + companionSuccess: 'companionSuccess', + companionWarning: 'companionWarning', + copyContainer: 'copyContainer', + copyBlock: 'copyBlock', + clipboardInsertBlock: 'clipboardInsertBlock', + clipboardInsertContainer: 'clipboardInsertContainer', + loadStructuralElement: 'loadStructuralElement', + loadContainer: 'loadContainer', + deleteClipboard: 'courseware-clipboards/delete', + updateClipboard: 'courseware-clipboards/update', + loadClipboard: 'courseware-clipboards/loadById', + }), + + async insertItem() { + let insertError = false; + + if (this.isBlock) { + if (!this.blockAdderActive) { + this.companionWarning({ info: this.text.positionWarning }); + return; + } + try { + await this.clipboardInsertBlock({ + parentId: this.blockAdder.container.id, + section: this.blockAdder.section, + clipboard: this.clipboard, + }); + } catch (error) { + insertError = true; + this.companionWarning({ info: this.text.errorMessage }); + } + if (!insertError) { + await this.loadContainer(this.blockAdder.container.id); + this.companionSuccess({ info: this.text.blockSuccess }); + } + } else { + try { + await this.clipboardInsertContainer({ + parentId: this.currentElement, + clipboard: this.clipboard, + }); + } catch (error) { + insertError = true; + this.companionWarning({ info: this.text.errorMessage }); + } + if (!insertError) { + this.loadStructuralElement(this.currentElement); + this.companionSuccess({ info: this.text.containerSuccess }); + } + } + }, + + async insertItemCopy() { + let insertError = false; + + if (this.isBlock) { + if (!this.blockAdderActive) { + this.companionWarning({ info: this.text.positionWarning }); + return; + } + try { + await this.copyBlock({ + parentId: this.blockAdder.container.id, + section: this.blockAdder.section, + block: { id: this.blockId }, + }); + } catch (error) { + insertError = true; + this.companionWarning({ info: this.text.errorMessage }); + } + if (!insertError) { + await this.loadContainer(this.blockAdder.container.id); + this.companionSuccess({ info: this.text.blockSuccess }); + } + } else { + try { + await this.copyContainer({ parentId: this.currentElement, container: { id: this.containerId } }); + } catch (error) { + insertError = true; + this.companionWarning({ info: this.text.errorMessage }); + } + if (!insertError) { + this.loadStructuralElement(this.currentElement); + this.companionSuccess({ info: this.text.containerSuccess }); + } + } + }, + deleteItem() { + this.deleteClipboard({ id: this.clipboard.id }); + }, + showEditItem() { + this.showEditDialog = true; + }, + closeEditItem() { + this.showEditDialog = false; + }, + async storeItem() { + this.closeEditItem(); + await this.updateClipboard(this.currentClipboard); + this.loadClipboard({ id: this.currentClipboard.id }); + }, + }, + mounted() { + this.currentClipboard = _.cloneDeep(this.clipboard); + }, +}; +</script> diff --git a/resources/vue/components/courseware/CoursewareContainerActions.vue b/resources/vue/components/courseware/CoursewareContainerActions.vue index 87c0e12a5e608ccc41ccde6b79af61476e5d1270..2480abaaeee2a50ddd37b7a02cca398083de93d3 100644 --- a/resources/vue/components/courseware/CoursewareContainerActions.vue +++ b/resources/vue/components/courseware/CoursewareContainerActions.vue @@ -7,6 +7,7 @@ @changeContainer="changeContainer" @deleteContainer="deleteContainer" @removeLock="removeLock" + @copyToClipboard="copyToClipboard" /> </div> </template> @@ -43,12 +44,13 @@ export default { menuItems.push({ id: 1, label: this.$gettext('Abschnitt bearbeiten'), icon: 'edit', emit: 'editContainer' }); } menuItems.push({ id: 2, label: this.$gettext('Abschnitt verändern'), icon: 'settings', emit: 'changeContainer' }); - menuItems.push({ id: 3, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' }); + menuItems.push({ id: 3, label: this.$gettext('Abschnitt merken'), icon: 'clipboard', emit: 'copyToClipboard' }); + menuItems.push({ id: 5, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' }); } if (this.blocked && this.blockedByAnotherUser && this.userIsTeacher) { menuItems.push({ - id: 4, + id: 3, label: this.$gettext('Sperre aufheben'), icon: 'lock-unlocked', emit: 'removeLock', @@ -77,6 +79,9 @@ export default { }, removeLock() { this.$emit('removeLock'); + }, + copyToClipboard() { + this.$emit('copyToClipboard'); } }, }; diff --git a/resources/vue/components/courseware/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/CoursewareDefaultBlock.vue index ff9701986c346c7e2d1b949dd3a70306bd811931..eb592ac8e01596b73eb072c6c0169ac5f13bf1ef 100644 --- a/resources/vue/components/courseware/CoursewareDefaultBlock.vue +++ b/resources/vue/components/courseware/CoursewareDefaultBlock.vue @@ -23,6 +23,7 @@ @showExportOptions="displayFeature('ExportOptions')" @deleteBlock="displayDeleteDialog()" @removeLock="displayRemoveLockDialog()" + @copyToClipboard="copyToClipboard" /> </header> <div v-show="isOpen"> @@ -201,12 +202,14 @@ export default { ...mapActions({ companionInfo: 'companionInfo', companionWarning: 'companionWarning', + companionSuccess: 'companionSuccess', deleteBlock: 'deleteBlockInContainer', lockObject: 'lockObject', unlockObject: 'unlockObject', loadContainer: 'loadContainer', loadBlock: 'courseware-blocks/loadById', updateContainer: 'updateContainer', + createClipboard: 'courseware-clipboards/create' }), async displayFeature(element) { if (this.showEdit && element === 'Edit') { @@ -358,6 +361,19 @@ export default { await this.unlockObject({ id: this.block.id , type: 'courseware-blocks' }); await this.loadBlock({ id: this.block.id }); this.showRemoveLockDialog = false; + }, + async copyToClipboard() { + const clipboard = { + attributes: { + name: this.block.attributes.title, + 'block-id': this.block.id, + 'object-type': this.block.type, + 'object-kind': this.block.attributes['block-type'], + } + }; + + await this.createClipboard(clipboard, { root: true }); + this.companionSuccess({ info: this.$gettext('Block wurde in Merkliste abgelegt.') }); } }, diff --git a/resources/vue/components/courseware/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/CoursewareDefaultContainer.vue index a3f99d0f07220a19904720102fb1f007599ef706..313a9788ca88f4da9820496c62809dce002e4fdf 100644 --- a/resources/vue/components/courseware/CoursewareDefaultContainer.vue +++ b/resources/vue/components/courseware/CoursewareDefaultContainer.vue @@ -20,6 +20,7 @@ @changeContainer="displayChangeDialog" @deleteContainer="displayDeleteDialog" @removeLock="displayRemoveLockDialog" + @copyToClipboard="copyToClipboard" /> </header> <div v-show="isOpen" @@ -214,12 +215,14 @@ export default { ...mapActions({ companionInfo: 'companionInfo', companionWarning: 'companionWarning', + companionSuccess: 'companionSuccess', updateContainer: 'updateContainer', loadContainer: 'courseware-containers/loadById', deleteContainer: 'deleteContainer', lockObject: 'lockObject', unlockObject: 'unlockObject', - coursewareBlockAdder: 'coursewareBlockAdder' + coursewareBlockAdder: 'coursewareBlockAdder', + createClipboard: 'courseware-clipboards/create' }), async displayEditDialog() { await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } }); @@ -362,6 +365,21 @@ export default { this.showRemoveLockDialog = false; }, + async copyToClipboard() { + const clipboard = { + attributes: { + name: this.container.attributes.title, + 'container-id': this.container.id, + 'object-type': this.container.type, + 'object-kind': this.container.attributes['container-type'], + + } + }; + + await this.createClipboard(clipboard, { root: true }); + this.companionSuccess({ info: this.$gettext('Abschnitt wurde in Merkliste abgelegt.') }); + } + }, watch: { diff --git a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue index 15adf2bdcb489d96af277b8c3c859e523df8e2ae..36348a38f171fc1d76818df909234b81e5e1686c 100644 --- a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue +++ b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue @@ -99,7 +99,57 @@ :secondSection="$gettext('zweites Element')" ></courseware-container-adder-item> </courseware-tab> + <courseware-tab :name="$gettext('Merkliste')" :selected="showClipboard" :index="2" :style="{ maxHeight: maxHeight + 'px' }"> + <courseware-collapsible-box :title="$gettext('Blöcke')" :open="clipboardBlocks.length > 0"> + <template v-if="clipboardBlocks.length > 0"> + <div class="cw-element-inserter-wrapper"> + <courseware-clipboard-item + v-for="(clipboard, index) in clipboardBlocks" + :key="index" + :clipboard="clipboard" + @inserted="$emit('blockAdded')" + /> + </div> + <button class="button trash" @click="clearClipboard('courseware-blocks')"> + {{ $gettext('Alle Blöcke aus Merkliste entfernen') }} + </button> + </template> + <courseware-companion-box + v-else + mood="pointing" + :msgCompanion="$gettext('Die Merkliste enthält keine Blöcke.')" + /> + </courseware-collapsible-box> + <courseware-collapsible-box :title="$gettext('Abschnitte')" :open="clipboardContainers.length > 0"> + <template v-if="clipboardContainers.length > 0"> + <div class="cw-element-inserter-wrapper"> + <courseware-clipboard-item + v-for="(clipboard, index) in clipboardContainers" + :key="index" + :clipboard="clipboard" + /> + </div> + <button class="button trash" @click="clearClipboard('courseware-containers')"> + {{ $gettext('Alle Abschnitte aus Merkliste entfernen') }} + </button> + </template> + <courseware-companion-box + v-else + mood="pointing" + :msgCompanion="$gettext('Die Merkliste enthält keine Abschnitte.')" + /> + </courseware-collapsible-box> + </courseware-tab> </courseware-tabs> + <studip-dialog + v-if="showDeleteClipboardDialog" + :title="textDeleteClipboardTitle" + :question="textDeleteClipboardAlert" + height="200" + width="500" + @confirm="executeDeleteClipboard" + @close="closeDeleteClipboardDialog" + ></studip-dialog> </div> </template> @@ -107,18 +157,26 @@ import CoursewareTabs from './CoursewareTabs.vue'; import CoursewareTab from './CoursewareTab.vue'; import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue'; +import CoursewareClipboardItem from './CoursewareClipboardItem.vue'; import CoursewareContainerAdderItem from './CoursewareContainerAdderItem.vue'; import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; +import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue'; import { mapActions, mapGetters } from 'vuex'; +import StudipDialog from '../StudipDialog.vue'; + export default { name: 'cw-tools-blockadder', components: { CoursewareTabs, CoursewareTab, CoursewareBlockadderItem, + CoursewareClipboardItem, CoursewareContainerAdderItem, CoursewareCompanionBox, + CoursewareCollapsibleBox, + + StudipDialog, }, props: { stickyRibbon: { @@ -130,11 +188,14 @@ export default { return { showBlockadder: true, showContaineradder: false, + showClipboard: false, searchInput: '', currentFilterCategory: '', filteredBlockTypes: [], categorizedBlocks: [], - selectedContainerStyle: 'full' + selectedContainerStyle: 'full', + showDeleteClipboardDialog: false, + deleteClipboardType: null }; }, computed: { @@ -145,6 +206,8 @@ export default { containerTypes: 'containerTypes', favoriteBlockTypes: 'favoriteBlockTypes', showToolbar: 'showToolbar', + usersClipboards: 'courseware-clipboards/all', + userId: 'userId' }), blockTypes() { let blockTypes = JSON.parse(JSON.stringify(this.unorderedBlockTypes)); @@ -177,6 +240,34 @@ export default { } else { return parseInt(Math.min(window.innerHeight * 0.75, window.innerHeight - 197)) - 120; } + }, + clipboardBlocks() { + return this.usersClipboards + .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-blocks') + .sort((a, b) => b.attributes.mkdate - a.attributes.mkdate); + }, + clipboardContainers() { + return this.usersClipboards + .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-containers') + .sort((a, b) => b.attributes.mkdate < a.attributes.mkdate); + }, + textDeleteClipboardTitle() { + if (this.deleteClipboardType === 'courseware-blocks') { + return this.$gettext('Merkliste für Blöcke leeren'); + } + if (this.deleteClipboardType === 'courseware-containers') { + return this.$gettext('Merkliste für Abschnitte leeren'); + } + return ''; + }, + textDeleteClipboardAlert() { + if (this.deleteClipboardType === 'courseware-blocks') { + return this.$gettext('Möchten Sie die Merkliste für Blöcke unwiderruflich leeren?'); + } + if (this.deleteClipboardType === 'courseware-containers') { + return this.$gettext('Möchten Sie die Merkliste für Abschnitte unwiderruflich leeren?'); + } + return ''; } }, methods: { @@ -184,17 +275,23 @@ export default { removeFavoriteBlockType: 'removeFavoriteBlockType', addFavoriteBlockType: 'addFavoriteBlockType', coursewareContainerAdder: 'coursewareContainerAdder', - companionWarning: 'companionWarning' + companionWarning: 'companionWarning', + deleteUserClipboards: 'deleteUserClipboards' }), displayContainerAdder() { this.showContaineradder = true; this.showBlockadder = false; + this.showClipboard = false; }, displayBlockAdder() { this.showContaineradder = false; + this.showClipboard = false; this.showBlockadder = true; this.disableContainerAdder(); }, + displayClipboard() { + this.showClipboard = true; + }, isBlockFav(block) { let isFav = false; this.favoriteBlockTypes.forEach((type) => { @@ -279,6 +376,20 @@ export default { this.filteredBlockTypes = this.blockTypes; this.searchInput = ''; this.currentFilterCategory = ''; + }, + clearClipboard(type) { + this.deleteClipboardType = type; + this.showDeleteClipboardDialog = true; + }, + executeDeleteClipboard() { + if (this.deleteClipboardType) { + this.deleteUserClipboards({uid: this.userId, type: this.deleteClipboardType}); + } + this.closeDeleteClipboardDialog(); + }, + closeDeleteClipboardDialog() { + this.showDeleteClipboardDialog = false; + this.deleteClipboardType = null; } }, mounted() { diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js index 5b66ef7d1954897069fc8623193dc436b5dcf65a..4741cd46974c6f437e97ee4e99bc442f506824c4 100644 --- a/resources/vue/courseware-index-app.js +++ b/resources/vue/courseware-index-app.js @@ -92,6 +92,7 @@ const mountApp = async (STUDIP, createApp, element) => { 'courseware-blocks', 'courseware-block-comments', 'courseware-block-feedback', + 'courseware-clipboards', 'courseware-containers', 'courseware-instances', 'courseware-public-links', @@ -144,11 +145,19 @@ const mountApp = async (STUDIP, createApp, element) => { store.dispatch('oerEnabled', oer_enabled); store.dispatch('licenses', licenses); store.dispatch('courseware-templates/loadAll'); + store.dispatch('loadUserClipboards', STUDIP.USER_ID); const pluginManager = new PluginManager(); store.dispatch('setPluginManager', pluginManager); STUDIP.eventBus.emit('courseware:init-plugin-manager', pluginManager); + STUDIP.JSUpdater.register( + 'coursewareclipboard', + () => { store.dispatch('loadUserClipboards', STUDIP.USER_ID)}, + () => { return { 'counter' : store.getters['courseware-clipboards/all'].length };}, + 5000 + ); + const app = createApp({ render: (h) => h(IndexApp), router, diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index fc688f015ea882ccd75e69497e41adb848ef89a8..9669a6138b18d36d236f056c23bc4a36d7c2e7d1 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -429,11 +429,12 @@ export const actions = { return dispatch('folders/loadById', { id: folderId, options }, { root: true }); }, - copyBlock({ getters }, { parentId, block }) { + copyBlock({ getters }, { parentId, block, section }) { const copy = { data: { block: block, parent_id: parentId, + section: section }, }; @@ -441,6 +442,16 @@ export const actions = { // console.log(resp); }); }, + clipboardInsertBlock({ getters }, { parentId, clipboard, section }) { + const insert = { + data: { + parent_id: parentId, + section: section + }, + }; + + return state.httpClient.post(`courseware-clipboards/${clipboard.id}/insert`, insert); + }, copyContainer({ getters }, { parentId, container }) { const copy = { data: { @@ -453,6 +464,15 @@ export const actions = { // console.log(resp); }); }, + clipboardInsertContainer({ getters },{ parentId, clipboard }) { + const insert = { + data: { + parent_id: parentId, + }, + }; + + return state.httpClient.post(`courseware-clipboards/${clipboard.id}/insert`, insert); + }, async copyStructuralElement({ dispatch, getters, rootGetters }, { parentId, elementId, removePurpose, migrate, modifications }) { const copy = { data: { parent_id: parentId, remove_purpose: removePurpose, migrate: migrate, modifications: modifications } }; @@ -1354,6 +1374,25 @@ export const actions = { async loadProgresses({ dispatch, commit, getters }) { const progresses = await dispatch('loadUnitProgresses', { unitId: getters.context.unit }); commit('setProgresses', progresses); + }, + + loadUserClipboards({ dispatch }, uid) { + dispatch('courseware-clipboards/resetState'); + const parent = { type: 'users', id: uid }; + const relationship = 'courseware-clipboards'; + const options = {} + + return dispatch('loadRelatedPaginated', { + type: 'courseware-clipboards', + parent, + relationship, + options, + }); + }, + + async deleteUserClipboards({ dispatch, rootGetters }, { uid, type }) { + await state.httpClient.delete(`users/${uid}/courseware-clipboards/${type}`); + dispatch('loadUserClipboards', uid); } };