diff --git a/db/migrations/6.0.27_add_unit_permissions.php b/db/migrations/6.0.27_add_unit_permissions.php new file mode 100644 index 0000000000000000000000000000000000000000..c275a58561f3efb09e9816bec5124aebb8f165de --- /dev/null +++ b/db/migrations/6.0.27_add_unit_permissions.php @@ -0,0 +1,93 @@ +<?php + +final class AddUnitPermissions extends Migration +{ + public function description() + { + return 'Add cols to cw_units table for permission settings'; + } + + public function up() + { + \DBManager::get()->exec("ALTER TABLE `cw_units` + ADD `permission_scope` ENUM('unit', 'structural_element') COLLATE latin1_bin NOT NULL DEFAULT 'unit' + AFTER `creator_id` + "); + \DBManager::get()->exec("ALTER TABLE `cw_units` + ADD `permission_type` ENUM('all', 'users', 'groups') COLLATE latin1_bin NOT NULL DEFAULT 'all' + AFTER `permission_scope` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + ADD `visible` ENUM('always', 'never', 'period') COLLATE latin1_bin NOT NULL DEFAULT 'always' + AFTER `permission_type` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + ADD `visible_all` TINYINT NOT NULL DEFAULT 0 + AFTER `visible` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + ADD `writable` ENUM('always', 'never', 'period') COLLATE latin1_bin NOT NULL DEFAULT 'never' + AFTER `withdraw_date` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + ADD `writable_all` TINYINT NOT NULL DEFAULT 0 + AFTER `writable` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + ADD `writable_start_date` INT UNSIGNED DEFAULT NULL + AFTER `writable_all` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + ADD `writable_end_date` INT UNSIGNED DEFAULT NULL + AFTER `writable_start_date` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + ADD `visible_approval` TEXT NOT NULL + AFTER `writable_end_date` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + ADD `writable_approval` TEXT NOT NULL + AFTER `visible_approval` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + CHANGE `release_date` `visible_start_date` INT UNSIGNED DEFAULT NULL + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + CHANGE `withdraw_date` `visible_end_date` INT UNSIGNED DEFAULT NULL + "); + + } + + public function down() + { + \DBManager::get()->exec("ALTER TABLE `cw_units` DROP `permission_scope`"); + \DBManager::get()->exec("ALTER TABLE `cw_units` DROP `permission_type`"); + \DBManager::get()->exec("ALTER TABLE `cw_units` DROP `visible`"); + \DBManager::get()->exec("ALTER TABLE `cw_units` DROP `visible_all`"); + \DBManager::get()->exec("ALTER TABLE `cw_units` DROP `writable`"); + \DBManager::get()->exec("ALTER TABLE `cw_units` DROP `writable_all`"); + \DBManager::get()->exec("ALTER TABLE `cw_units` DROP `writable_start_date`"); + \DBManager::get()->exec("ALTER TABLE `cw_units` DROP `writable_end_date`"); + \DBManager::get()->exec("ALTER TABLE `cw_units` DROP `visible_approval`"); + \DBManager::get()->exec("ALTER TABLE `cw_units` DROP `writable_approval`"); + + + \DBManager::get()->exec("ALTER TABLE `cw_units` + CHANGE `visible_start_date` `release_date` INT UNSIGNED DEFAULT NULL + "); + + \DBManager::get()->exec("ALTER TABLE `cw_units` + CHANGE `visible_end_date` `withdraw_date` INT UNSIGNED DEFAULT NULL + "); + } +} \ No newline at end of file diff --git a/db/migrations/6.0.28_add_structural_element_permissions.php b/db/migrations/6.0.28_add_structural_element_permissions.php new file mode 100644 index 0000000000000000000000000000000000000000..8c5b5dd920994107b6b98908e6edc95a03b7391e --- /dev/null +++ b/db/migrations/6.0.28_add_structural_element_permissions.php @@ -0,0 +1,100 @@ +<?php + +final class AddStructuralElementPermissions extends Migration +{ + + public function description() + { + return 'Add cols to structural element for permission settings'; + } + public function up() + { + // add cols + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + ADD `permission_type` ENUM('all', 'users', 'groups') COLLATE latin1_bin NOT NULL DEFAULT 'all' + AFTER `commentable` + "); + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + ADD `visible` ENUM('always', 'never', 'period') COLLATE latin1_bin NOT NULL DEFAULT 'always' + AFTER `permission_type` + "); + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + ADD `visible_all` TINYINT NOT NULL DEFAULT 0 + AFTER `visible` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + ADD `writable` ENUM('always', 'never', 'period') COLLATE latin1_bin NOT NULL DEFAULT 'never' + AFTER `withdraw_date` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + ADD `writable_all` TINYINT NOT NULL DEFAULT 0 + AFTER `writable` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + ADD `writable_start_date` INT UNSIGNED NULL DEFAULT NULL + AFTER `writable_all` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + ADD `writable_end_date` INT UNSIGNED NULL DEFAULT NULL + AFTER `writable_start_date` + "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + ADD `content_approval` TEXT NOT NULL + AFTER `write_approval` + "); + + // change cols + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + CHANGE `release_date` `visible_start_date` INT UNSIGNED NULL DEFAULT NULL + "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + CHANGE `withdraw_date` `visible_end_date` INT UNSIGNED NULL DEFAULT NULL + "); + + \DBManager::get()->exec("UPDATE `cw_structural_elements` SET `visible_start_date` = NULL WHERE `visible_start_date` = 0 "); + + \DBManager::get()->exec("UPDATE `cw_structural_elements` SET `visible_end_date` = NULL WHERE `visible_end_date` = 0 "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + CHANGE `read_approval` `visible_approval` TEXT NOT NULL + "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + CHANGE `write_approval` `writable_approval` TEXT NOT NULL + "); + } + + public function down() + { + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` DROP `visible`"); + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` DROP `visible_all`"); + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` DROP `writable`"); + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` DROP `writable_all`"); + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` DROP `writable_start_date`"); + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` DROP `writable_end_date`"); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + CHANGE `visible_start_date` `release_date` INT UNSIGNED DEFAULT NULL + "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + CHANGE `visible_end_date` `withdraw_date` INT UNSIGNED DEFAULT NULL + "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + CHANGE `visible_approval` `read_approval` TEXT NOT NULL + "); + + \DBManager::get()->exec("ALTER TABLE `cw_structural_elements` + CHANGE `writable_approval` `write_approval` TEXT NOT NULL + "); + + } + +} \ No newline at end of file diff --git a/db/migrations/6.0.29_update_structural_element_permissions.php b/db/migrations/6.0.29_update_structural_element_permissions.php new file mode 100644 index 0000000000000000000000000000000000000000..5de86305ea58e1a356438ccb7b7fa5f1fc3c3fd2 --- /dev/null +++ b/db/migrations/6.0.29_update_structural_element_permissions.php @@ -0,0 +1,75 @@ +<?php + +final class UpdateStructuralElementPermissions extends Migration +{ + public function description() + { + return 'Update structural element permissions for new settings'; + } + public function up() + { + $query = "SELECT * FROM `cw_structural_elements` WHERE `visible_approval` != '[]' AND `writable_approval` != '[]'"; + $rows_statement = DBManager::get()->prepare($query); + $rows = $rows_statement->execute(); + + $query = "UPDATE `cw_structural_elements` + SET + `permission_type` = :permission_type, + `visible` = :visible, + `writable` = :writable + `visible_approval` = :visible_approval, + `writable_approval` = :writable_approval, + WHERE `id` = :id"; + $statement = DBManager::get()->prepare($query); + + foreach ($rows as $row) { + $read_approval = json_decode($row['visible_approval'], true) ?: []; + $write_approval = json_decode($row['writable_approval'], true) ?: []; + $permission_type = $row['permission_type']; + $visible = $row['visible']; + $writable = $row['writable']; + $visible_approval = []; + $writable_approval = []; + if (!$read_approval['all'] && $write_approval['all']) { + $writable = 'always'; + } + + if (count($read_approval['groups']) || count($write_approval['groups'])) { + $permission_type = 'groups'; + $writable = 'always'; + $visible_approval = $read_approval['groups']; + $writable_approval = $write_approval['groups']; + } + if (count($read_approval['users']) || count($write_approval['users'])) { + $permission_type = 'users'; + $writable = 'always'; + $visible_approval = $read_approval['users']; + $writable_approval = $write_approval['users']; + } + + $statement->bindValue(':permission_type', $permission_type); + $statement->bindValue(':visible', $visible); + $statement->bindValue(':writable', $writable); + $statement->bindValue(':visible_approval', json_encode($visible_approval)); + $statement->bindValue(':writable_approval', json_encode($writable_approval)); + $statement->execute(); + } + + $query = "SELECT * FROM `cw_structural_elements` WHERE `visible_start_date` IS NOT NULL OR `visible_end_date` IS NOT NULL"; + $rows_statement = DBManager::get()->prepare($query); + $rows = $rows_statement->execute(); + + $query = "UPDATE `cw_structural_elements` + SET + `visible` = :visible, + WHERE `id` = :id"; + $statement = DBManager::get()->prepare($query); + + foreach ($rows as $row) { + $visible = 'period'; + + $statement->bindValue(':visible', $visible); + $statement->execute(); + } + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Courseware/CoursesUnitsIndex.php b/lib/classes/JsonApi/Routes/Courseware/CoursesUnitsIndex.php index b09d0c8b5c11228c10730504dfb80cf64afd22d0..9a3425f1cb4cee97f5a46f3e1200f8c7205cd739 100644 --- a/lib/classes/JsonApi/Routes/Courseware/CoursesUnitsIndex.php +++ b/lib/classes/JsonApi/Routes/Courseware/CoursesUnitsIndex.php @@ -39,9 +39,15 @@ class CoursesUnitsIndex extends JsonApiController } $resources = Unit::findCoursesUnits($course); - $total = count($resources); + $readable_resources = []; + foreach ($resources as $resource) { + if ($resource->canRead($user)) { + $readable_resources[] = $resource; + } + } + $total = count($readable_resources); [$offset, $limit] = $this->getOffsetAndLimit(); - return $this->getPaginatedContentResponse(array_slice($resources, $offset, $limit), $total); + return $this->getPaginatedContentResponse(array_slice($readable_resources, $offset, $limit), $total); } } diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php index c038c45f847fe01ccbd36c1284c2fcbd225451b8..99481f57fd888c7069aee8741311dcb1c640a27e 100644 --- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php @@ -81,8 +81,18 @@ class StructuralElementsCreate extends JsonApiController 'title' => self::arrayGet($json, 'data.attributes.title', ''), 'purpose' => self::arrayGet($json, 'data.attributes.purpose', $parent->purpose), 'payload' => self::arrayGet($json, 'data.attributes.payload', ''), - 'read_approval' => $parent->read_approval, - 'write_approval' => $parent->write_approval, + 'permission_type'=> $parent->permission_type, + 'visible' => $parent->visible, + 'visible_all' => $parent->visible_all, + 'visible_start_date' => $parent->visible_start_date, + 'visible_end_date' => $parent->visible_end_date, + 'writable' => $parent->writable, + 'writable_all' => $parent->writable_all, + 'writable_start_date' => $parent->writable_start_date, + 'writable_end_date' => $parent->writable_end_date, + 'visible_approval' => $parent->visible_approval, + 'writable_approval' => $parent->writable_approval, + 'content_approval' => $parent->content_approval, 'position' => $parent->countChildren(), 'commentable' => 0 ]); diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php index b4a8e1c1108c7299642a180cbebb522f20a44ee0..699623b03a2fe691b12d6c9b19c3724c4ab3e5dc 100644 --- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php @@ -49,7 +49,7 @@ class StructuralElementsReleasedIndex extends JsonApiController ); foreach ($contents as $content) { - if ((count($content->read_approval) && count($content->read_approval['users']) > 0) || (count($content->write_approval) && count($content->write_approval['users']) > 0)) { + if (count($content->content_approval) && count($content->content_approval['users']) > 0) { $resources[] = $content; } } diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php index 1582fbfb18d42118dfb74e913aac067cbcfc68b1..9ebf923d450b63dd17cc3e2c604e6a6cb1ceb76e 100644 --- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php @@ -49,19 +49,13 @@ class StructuralElementsSharedIndex extends JsonApiController ); foreach ($contents as $content) { - if (!count($content->read_approval) || !count($content->write_approval)) { + if (count($content->content_approval) === 0) { continue; } $add_content = false; - foreach ($content->read_approval['users'] as $listedUserPerm) { - if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read']) { - $add_content = true; - } - } - - foreach ($content->write_approval['users'] as $listedUserPerm) { + foreach ($content->content_approval['users'] as $listedUserPerm) { if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read']) { $add_content = true; } diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsUpdate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsUpdate.php index 455aacc3c06bc2a4e14aacc910e6b4b7b9126bda..3b3deb0a920b711eacb9c6beecb9845ff417622f 100644 --- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsUpdate.php @@ -6,6 +6,7 @@ use Courseware\StructuralElement; use JsonApi\Errors\AuthorizationFailedException; use JsonApi\Errors\RecordNotFoundException; use JsonApi\JsonApiController; +use JsonApi\Routes\TimestampTrait; use JsonApi\Routes\ValidationTrait; use JsonApi\Schemas\Courseware\StructuralElement as StructuralElementSchema; use JsonApi\Schemas\FileRef as FileRefSchema; @@ -19,6 +20,7 @@ use Psr\Http\Message\ServerRequestInterface as Request; class StructuralElementsUpdate extends JsonApiController { use EditBlockAwareTrait; + use TimestampTrait; use ValidationTrait; /** @@ -26,7 +28,8 @@ class StructuralElementsUpdate extends JsonApiController */ public function __invoke(Request $request, Response $response, $args) { - if (!($resource = StructuralElement::find($args['id']))) { + $resource = StructuralElement::find($args['id']); + if (!$resource) { throw new RecordNotFoundException(); } $json = $this->validate($request, $resource); @@ -105,7 +108,7 @@ class StructuralElementsUpdate extends JsonApiController } $parentId = self::arrayGet($json, 'data.relationships.parent.data.id'); - return \Courseware\StructuralElement::find($parentId); + return StructuralElement::find($parentId); } private function updateStructuralElement(\User $user, StructuralElement $resource, array $json): StructuralElement @@ -118,11 +121,13 @@ class StructuralElementsUpdate extends JsonApiController 'position', 'public', 'purpose', - 'read-approval', - 'release-date', 'title', - 'withdraw-date', - 'write-approval', + 'permission-type', + 'visible', + 'writable', + 'visible-approval', + 'writable-approval', + 'content-approval', ]; foreach ($attributes as $jsonKey) { @@ -131,13 +136,43 @@ class StructuralElementsUpdate extends JsonApiController $resource->$sormKey = $val; } } - - if (isset($json['data']['attributes']['release-date'])) { - $resource->release_date = $json['data']['attributes']['release-date']; + if (self::arrayHas($json, 'data.attributes.visible-all')) { + $resource->visible_all = self::arrayGet($json, 'data.attributes.visible-all'); } - - if (isset($json['data']['attributes']['withdraw-date'])) { - $resource->withdraw_date = $json['data']['attributes']['withdraw-date']; + if (self::arrayHas($json, 'data.attributes.writable-all')) { + $resource->writable_all = self::arrayGet($json, 'data.attributes.writable-all'); + } + if (self::arrayHas($json, 'data.attributes.visible-start-date')) { + $visibleStartDate = self::arrayGet($json, 'data.attributes.visible-start-date'); + if ($visibleStartDate) { + $visibleStartDate = self::fromISO8601($visibleStartDate); + $visibleStartDate = $visibleStartDate->getTimestamp(); + } + $resource->visible_start_date = $visibleStartDate; + } + if (self::arrayHas($json, 'data.attributes.visible-end-date')) { + $visibleEndDate = self::arrayGet($json, 'data.attributes.visible-end-date'); + if ($visibleEndDate) { + $visibleEndDate = self::fromISO8601($visibleEndDate); + $visibleEndDate = $visibleEndDate->getTimestamp(); + } + $resource->visible_end_date = $visibleEndDate; + } + if (self::arrayHas($json, 'data.attributes.writable-start-date')) { + $writableStartDate = self::arrayGet($json, 'data.attributes.writable-start-date'); + if ($writableStartDate) { + $writableStartDate = self::fromISO8601($writableStartDate); + $writableStartDate = $writableStartDate->getTimestamp(); + } + $resource->writable_start_date = $writableStartDate; + } + if (self::arrayHas($json, 'data.attributes.writable-end-date')) { + $writableEndDate = self::arrayGet($json, 'data.attributes.writable-end-date'); + if ($writableEndDate) { + $writableEndDate = self::fromISO8601($writableEndDate); + $writableEndDate = $writableEndDate->getTimestamp(); + } + $resource->writable_end_date = $writableEndDate; } if (isset($json['data']['attributes']['commentable'])) { diff --git a/lib/classes/JsonApi/Routes/Courseware/UnitsCopy.php b/lib/classes/JsonApi/Routes/Courseware/UnitsCopy.php index 61ffa956cf2d8531c0013b164a7ce30c788dc419..62a1d0a81e2ba7fc2cb3b8bbe33332c1123c490a 100644 --- a/lib/classes/JsonApi/Routes/Courseware/UnitsCopy.php +++ b/lib/classes/JsonApi/Routes/Courseware/UnitsCopy.php @@ -31,6 +31,7 @@ class UnitsCopy extends NonJsonApiController $rangeId = $data['rangeId']; $rangeType = $data['rangeType']; $modified = $data['modified']; + $duplicate = $data['duplicate']; try { $range = \RangeFactory::createRange($rangeType, $rangeId); @@ -42,7 +43,7 @@ class UnitsCopy extends NonJsonApiController throw new AuthorizationFailedException(); } - $newUnit = $sourceUnit->copy($user, $rangeId, $rangeType, $modified); + $newUnit = $sourceUnit->copy($user, $rangeId, $rangeType, $modified, $duplicate); $response = $response->withHeader('Content-Type', 'application/json'); $response->getBody()->write((string) json_encode($newUnit)); diff --git a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php index 9dfd2e63c04a6820ccd44bc46dac0e54b9526687..8f3e217e8f386f6c7d1e59f5da2464a0cef432f9 100644 --- a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php @@ -97,7 +97,10 @@ class UnitsCreate extends JsonApiController 'purpose' => self::arrayGet($json, 'data.attributes.purpose', 'content'), 'payload' => self::arrayGet($json, 'data.attributes.payload', ''), 'position' => 0, - 'commentable' => 0 + 'commentable' => 0, + 'permission_type' => self::arrayGet($json, 'data.attributes.permission-type', 'all'), + 'visible' => self::arrayGet($json, 'data.attributes.visible', 'always'), + 'writable' => self::arrayGet($json, 'data.attributes.writable', 'never'), ]); \Courseware\Container::create([ @@ -114,7 +117,7 @@ class UnitsCreate extends JsonApiController ]), ]); - $unit = \Courseware\Unit::create([ + $unit = Unit::create([ 'range_id' => $range->getRangeId(), 'range_type' => $range->getRangeType(), 'structural_element_id' => $struct->id, @@ -122,8 +125,9 @@ class UnitsCreate extends JsonApiController 'position' => Unit::getNewPosition($range->getRangeId()), 'creator_id' => $user->id, 'public' => self::arrayGet($json, 'data.attributes.public', '0'), - 'release_date' => self::arrayGet($json, 'data.attributes.release-date'), - 'withdraw_date' => self::arrayGet($json, 'data.attributes.withdraw-date'), + 'permission_type' => self::arrayGet($json, 'data.attributes.permission-type', 'all'), + 'visible' => self::arrayGet($json, 'data.attributes.visible', 'always'), + 'writable' => self::arrayGet($json, 'data.attributes.writable', 'never'), ]); $instance = new \Courseware\Instance($struct); diff --git a/lib/classes/JsonApi/Routes/Courseware/UnitsUpdate.php b/lib/classes/JsonApi/Routes/Courseware/UnitsUpdate.php index 446d61e287279b00e1a04a39f863c793c92bd387..4c48086ae053af9a2135e022825699d60b817f70 100644 --- a/lib/classes/JsonApi/Routes/Courseware/UnitsUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/UnitsUpdate.php @@ -57,37 +57,92 @@ class UnitsUpdate extends JsonApiController return 'Document must have an `id`.'; } - if (self::arrayHas($json, 'data.attributes.release-date')) { - $releaseDate = self::arrayGet($json, 'data.attributes.release-date'); - if (!self::isValidTimestamp($releaseDate)) { - return '`release-date` is not an ISO 8601 timestamp.'; + if (self::arrayHas($json, 'data.attributes.visible-start-date')) { + $visibleStartDate = self::arrayGet($json, 'data.attributes.visible-start-date'); + if ($visibleStartDate && !self::isValidTimestamp($visibleStartDate)) { + return '`visible-start-date` is not an ISO 8601 timestamp.'; } } - if (self::arrayHas($json, 'data.attributes.withdraw-date')) { - $withdrawDate = self::arrayGet($json, 'data.attributes.withdraw-date'); - if (!self::isValidTimestamp($withdrawDate)) { - return '`withdraw-date` is not an ISO 8601 timestamp.'; + if (self::arrayHas($json, 'data.attributes.visible-end-date')) { + $visibleEndDate = self::arrayGet($json, 'data.attributes.visible-end-date'); + if ($visibleEndDate && !self::isValidTimestamp($visibleEndDate)) { + return '`visible-start-date` is not an ISO 8601 timestamp.'; + } + } + + if (self::arrayHas($json, 'data.attributes.writable-start-date')) { + $writableStartDate = self::arrayGet($json, 'data.attributes.writable-start-date'); + if ($writableStartDate && !self::isValidTimestamp($writableStartDate)) { + return '`writable-start-date` is not an ISO 8601 timestamp.'; + } + } + + if (self::arrayHas($json, 'data.attributes.writable-end-date')) { + $writableEndDate = self::arrayGet($json, 'data.attributes.writable-end-date'); + if ($writableEndDate && !self::isValidTimestamp($writableEndDate)) { + return '`writable-end-date` is not an ISO 8601 timestamp.'; } } } private function updateUnit(\User $user, Unit $resource, array $json): Unit { - if (self::arrayHas($json, 'data.attributes.public')) { - $resource->public = self::arrayGet($json, 'data.attributes.public'); - } + $attributes = [ + 'position', + 'public', + 'permission-scope', + 'permission-type', + 'visible', + 'visible-approval', + 'writable', + 'writable-approval', + ]; - if (self::arrayHas($json, 'data.attributes.release-date')) { - $releaseDate = self::arrayGet($json, 'data.attributes.release-date', ''); - $releaseDate = self::fromISO8601($releaseDate); - $resource->release_date = $releaseDate->getTimestamp(); + foreach ($attributes as $jsonKey) { + $sormKey = strtr($jsonKey, '-', '_'); + $val = self::arrayGet($json, 'data.attributes.' . $jsonKey, ''); + if ($val) { + $resource->$sormKey = $val; + } } - - if (self::arrayHas($json, 'data.attributes.withdraw-date')) { - $withdrawDate = self::arrayGet($json, 'data.attributes.withdraw-date', ''); - $withdrawDate = self::fromISO8601($withdrawDate); - $resource->withdraw_date = $withdrawDate->getTimestamp(); + if (self::arrayHas($json, 'data.attributes.visible-all')) { + $resource->visible_all = self::arrayGet($json, 'data.attributes.visible-all'); + } + if (self::arrayHas($json, 'data.attributes.writable-all')) { + $resource->writable_all = self::arrayGet($json, 'data.attributes.writable-all'); + } + if (self::arrayHas($json, 'data.attributes.visible-start-date')) { + $visibleStartDate = self::arrayGet($json, 'data.attributes.visible-start-date'); + if ($visibleStartDate) { + $visibleStartDate = self::fromISO8601($visibleStartDate); + $visibleStartDate = $visibleStartDate->getTimestamp(); + } + $resource->visible_start_date = $visibleStartDate; + } + if (self::arrayHas($json, 'data.attributes.visible-end-date')) { + $visibleEndDate = self::arrayGet($json, 'data.attributes.visible-end-date'); + if ($visibleEndDate) { + $visibleEndDate = self::fromISO8601($visibleEndDate); + $visibleEndDate = $visibleEndDate->getTimestamp(); + } + $resource->visible_end_date = $visibleEndDate; + } + if (self::arrayHas($json, 'data.attributes.writable-start-date')) { + $writableStartDate = self::arrayGet($json, 'data.attributes.writable-start-date'); + if ($writableStartDate) { + $writableStartDate = self::fromISO8601($writableStartDate); + $writableStartDate = $writableStartDate->getTimestamp(); + } + $resource->writable_start_date = $writableStartDate; + } + if (self::arrayHas($json, 'data.attributes.writable-end-date')) { + $writableEndDate = self::arrayGet($json, 'data.attributes.writable-end-date'); + if ($writableEndDate) { + $writableEndDate = self::fromISO8601($writableEndDate); + $writableEndDate = $writableEndDate->getTimestamp(); + } + $resource->writable_end_date = $writableEndDate; } $resource->store(); diff --git a/lib/classes/JsonApi/Routes/SemestersIndex.php b/lib/classes/JsonApi/Routes/SemestersIndex.php index ffa1d4c0f7b4737b730d34ae6cd86e2008f6cd96..24883c19f3dea43ce8452a1f039c5b89a72577fb 100644 --- a/lib/classes/JsonApi/Routes/SemestersIndex.php +++ b/lib/classes/JsonApi/Routes/SemestersIndex.php @@ -4,6 +4,7 @@ namespace JsonApi\Routes; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; use JsonApi\JsonApiController; /** @@ -13,10 +14,30 @@ class SemestersIndex extends JsonApiController { protected $allowedPagingParameters = ['offset', 'limit']; + protected $allowedFilteringParameters = ['current', 'timestamp']; + public function __invoke(Request $request, Response $response, $args) { list($offset, $limit) = $this->getOffsetAndLimit(); - $semesters = \Semester::getAll(); + + $filtering = $this->getQueryParameters()->getFilteringParameters(); + + if (empty($filtering)) { + $semesters = \Semester::getAll(); + } else { + if (array_key_exists('current', $filtering)) { + $semester = \Semester::findCurrent(); + } + if (isset($filtering['timestamp'])) { + $semester = \Semester::findByTimestamp($filtering['timestamp']); + } + + if (!$semester) { + throw new RecordNotFoundException('Could not find semester.'); + } else { + $semesters = [$semester]; + } + } return $this->getPaginatedContentResponse( array_slice($semesters, $offset, $limit), diff --git a/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php b/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php index e6ccafa2f88979c01ee9c586f1e86f837f6b59e1..f1a584137c6053e70cebab27d5c1ee095a641b47 100644 --- a/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php +++ b/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php @@ -47,10 +47,18 @@ class StructuralElement extends SchemaProvider 'purpose' => (string) $resource['purpose'], 'payload' => $resource['payload']->getIterator(), 'public' => (int) $resource['public'], - 'release-date' => $resource['release_date'] ? date('Y-m-d', (int) $resource['release_date']) : null, - 'withdraw-date' => $resource['withdraw_date'] ? date('Y-m-d', (int) $resource['withdraw_date']) : null, - 'read-approval' => $resource['read_approval']->getIterator(), - 'write-approval' => $resource['write_approval']->getIterator(), + 'permission-type' => (string) $resource['permission_type'], + 'visible' => (string) $resource['visible'], + 'visible-all' => (bool) $resource['visible_all'], + 'visible-start-date' => $resource['visible_start_date'] ? date('c', $resource['visible_start_date']) : null, + 'visible-end-date' => $resource['visible_end_date'] ? date('c', $resource['visible_end_date']) : null, + 'writable' => (string) $resource['writable'], + 'writable-all' => (bool) $resource['writable_all'], + 'writable-start-date' => $resource['writable_start_date'] ? date('c', $resource['writable_start_date']) : null, + 'writable-end-date' => $resource['writable_end_date'] ? date('c', $resource['writable_end_date']) : null, + 'visible-approval' => json_decode($resource['visible_approval']), + 'writable-approval' => json_decode($resource['writable_approval']), + 'content-approval' => $resource['content_approval']->getIterator(), 'copy-approval' => $resource['copy_approval']->getIterator(), 'can-edit' => $resource->canEdit($user), 'can-visit' => $resource->canVisit($user), diff --git a/lib/classes/JsonApi/Schemas/Courseware/Unit.php b/lib/classes/JsonApi/Schemas/Courseware/Unit.php index 6152e940e61edbccdcb8a611f5522d06ae209f65..a311ac8286b19074c0598df89ae5cf72612be63d 100644 --- a/lib/classes/JsonApi/Schemas/Courseware/Unit.php +++ b/lib/classes/JsonApi/Schemas/Courseware/Unit.php @@ -28,15 +28,30 @@ class Unit extends SchemaProvider */ public function getAttributes($resource, ContextInterface $context): iterable { + $user = $this->currentUser; + return [ 'content-type' => (string) $resource['content_type'], 'position' => (int) $resource['position'], 'public' => (int) $resource['public'], - 'release-date' => $resource['release_date'] ? date('c', $resource['release_date']) : null, - 'withdraw-date' => $resource['withdraw_date'] ? date('c', $resource['withdraw_date']) : null, + 'permission-scope' => (string) $resource['permission_scope'], + 'permission-type' => (string) $resource['permission_type'], + 'visible' => (string) $resource['visible'], + 'visible-all' => (bool) $resource['visible_all'], + 'visible-start-date' => $resource['visible_start_date'] ? date('c', $resource['visible_start_date']) : null, + 'visible-end-date' => $resource['visible_end_date'] ? date('c', $resource['visible_end_date']) : null, + 'writable' => (string) $resource['writable'], + 'writable-all' => (bool) $resource['writable_all'], + 'writable-start-date' => $resource['writable_start_date'] ? date('c', $resource['writable_start_date']) : null, + 'writable-end-date' => $resource['writable_end_date'] ? date('c', $resource['writable_end_date']) : null, + 'visible-approval' => json_decode($resource['visible_approval']), + 'writable-approval' => json_decode($resource['writable_approval']), 'config' => json_decode($resource['config']), - 'mkdate' => date('c', $resource['mkdate']), - 'chdate' => date('c', $resource['chdate']), + 'can-read' => $resource->canRead($user), + 'can-edit' => $resource->canEdit($user), + 'can-edit-content' => $resource->canEditContent($user), + 'mkdate' => date('c', $resource['mkdate']), + 'chdate' => date('c', $resource['chdate']), ]; } diff --git a/lib/models/Courseware/StructuralElement.php b/lib/models/Courseware/StructuralElement.php index 8e06a396af93aa52f362ba4a27090c0a3f5a4474..6f87e70d94dc3e5d43b50d6e6300d1e8912ebd0d 100644 --- a/lib/models/Courseware/StructuralElement.php +++ b/lib/models/Courseware/StructuralElement.php @@ -32,10 +32,18 @@ use User; * @property \JSONArrayObject $payload database column * @property int $public database column * @property int $commentable database column - * @property int $release_date database column - * @property int $withdraw_date database column - * @property \JSONArrayObject $read_approval database column - * @property \JSONArrayObject $write_approval database column + * @property string $permission_type database column + * @property string $visible database column + * @property bool $visible_all database column + * @property int $visible_start_date database column + * @property int $visible_end_date database column + * @property string $writable database column + * @property bool $writable_all database column + * @property int|null $writable_start_date database column + * @property int|null $writable_end_date database column + * @property \JSONArrayObject $visible_approval database column + * @property \JSONArrayObject $writable_approval database column + * @property \JSONArrayObject $content_approval database column * @property \JSONArrayObject $copy_approval database column * @property \JSONArrayObject $external_relations database column * @property int $mkdate database column @@ -60,8 +68,9 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac $config['db_table'] = 'cw_structural_elements'; $config['serialized_fields']['payload'] = JSONArrayObject::class; - $config['serialized_fields']['read_approval'] = JSONArrayObject::class; - $config['serialized_fields']['write_approval'] = JSONArrayObject::class; + $config['serialized_fields']['visible_approval'] = JSONArrayObject::class; + $config['serialized_fields']['writable_approval'] = JSONArrayObject::class; + $config['serialized_fields']['content_approval'] = JSONArrayObject::class; $config['serialized_fields']['copy_approval'] = JSONArrayObject::class; $config['serialized_fields']['external_relations'] = JSONArrayObject::class; @@ -276,37 +285,42 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac if ($this->range_id === $user->id) { return true; } - - return $this->hasWriteApproval($user); + + return $this->hasWriteContentApproval($user); case 'course': - $hasEditingPermission = $this->hasEditingPermission($user); - if ($this->isTask()) { - $task = $this->task; - if (!$task) { - $task = $this->findParentTask(); + $unit = $this->findUnit(); + if ($unit->permission_scope === 'unit') { + return $unit->canEditContent($user); + } else { + $hasEditingPermission = $this->hasEditingPermission($user, $unit); + if ($this->isTask()) { + $task = $this->task; if (!$task) { + $task = $this->findParentTask(); + if (!$task) { + return false; + } + } + + if ($hasEditingPermission) { return false; } - } - if ($hasEditingPermission) { - return false; - } + if ($task->isSubmitted()) { + return false; + } - if ($task->isSubmitted()) { - return false; + return $task->userIsASolver($user); } - return $task->userIsASolver($user); - } + if ($hasEditingPermission) { + return true; + } - if ($hasEditingPermission) { - return true; + return $this->hasWriteApproval($user); } - return $this->hasWriteApproval($user); - default: throw new \InvalidArgumentException('Unknown range type.'); } @@ -337,22 +351,23 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac return true; } - return $this->hasReadApproval($user); + return $this->hasReadContentApproval($user); case 'course': - if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) { - return false; - } + $unit = $this->findUnit(); + if ($unit->permission_scope === 'unit') { + return $unit->canRead($user); + } else { + if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) { + return false; + } - if ($this->canEdit($user)) { - return true; - } + if ($this->canEdit($user)) { + return true; + } - if (!$this->releasedForReaders($this)) { - return false; + return $this->hasReadApproval($user); } - return $this->hasReadApproval($user); - default: throw new \InvalidArgumentException('Unknown range type.'); } @@ -371,7 +386,7 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac return true; } - return $this->hasReadApproval($user); + return $this->hasReadContentApproval($user); case 'course': if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) { @@ -411,10 +426,6 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac return true; } - if (!$this->releasedForReaders($this)) { - return false; - } - return $this->hasReadApproval($user) && $this->canReadSequential($user); default: @@ -425,9 +436,11 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac /** * @param \User|\Seminar_User $user */ - public function hasEditingPermission($user): bool + public function hasEditingPermission(User $user, Unit $unit = null): bool { - $unit = $this->findUnit(); + if (!isset($unit)) { + $unit = $this->findUnit(); + } return $GLOBALS['perm']->have_perm('root', $user->id) || $GLOBALS['perm']->have_studip_perm( $unit->config['editing_permission'] ?? 'tutor', @@ -438,34 +451,21 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac private function hasReadApproval($user): bool { - // this property is shared between all range types. - if ($this->read_approval['all']) { - return true; - } - - // now we also check against the perms for contents. - if ($this->range_type === 'user') { - return $this->hasUserReadApproval($user); - } else { - if (!count($this->read_approval)) { + if ($this->permission_type === 'all' || $this->visible_all) { + if ($this->isVisible()) { return true; } - - // find user in users - $users = $this->read_approval['users']; - foreach ($users as $approvedUserId) { - if ($approvedUserId === $user->id) { - return true; - } - } - - // find user in groups - $groups = $this->read_approval['groups']; + return false; + } + if ($this->permission_type === 'users') { + return in_array($user->id, json_decode($this->visible_approval)) && $this->isVisible(); + } + if ($this->permission_type === 'groups') { + $groups = json_decode($this->visible_approval); foreach ($groups as $groupId) { - /** @var ?\Statusgruppen $group */ $group = \Statusgruppen::find($groupId); if ($group && $group->isMember($user->id)) { - return true; + return $this->isVisible(); } } } @@ -473,28 +473,38 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac return false; } - private function hasUserReadApproval($user): bool + private function isVisible(): bool + { + if ($this->visible === 'always') { + return true; + } + if ($this->visible === 'never') { + return false; + } + + return + (empty($this->visible_start_date) || $this->visible_start_date < strtotime('today')) + && (empty($this->visible_end_date) || $this->visible_end_date >= strtotime('today')); + } + + private function hasReadContentApproval($user): bool { - if (!count($this->read_approval)) { + if (count($this->content_approval) === 0) { if ($this->isRootNode()) { return false; } - return $this->parent->hasUserReadApproval($user); + return $this->parent->hasReadContentApproval($user); } // find user in users - $users = $this->read_approval['users']; + $users = $this->content_approval['users']; foreach ($users as $listedUserPerm) { - // now for contents, there is an expiry date defined. if (!empty($listedUserPerm['expiry']) && strtotime($listedUserPerm['expiry']) < strtotime('today')) { if ($this->isRootNode()) { return false; } - return $this->parent->hasUserReadApproval($user); + return $this->parent->hasReadContentApproval($user); } - // In order to have a record of the users in the perms list of contents, - // we keep a full perm record in read_approval column, and set read property to true or false, - // this won't apply to write_approval column. if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read'] == true) { return true; } @@ -505,33 +515,22 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac private function hasWriteApproval($user): bool { - // this property is shared between all range types. - if ($this->write_approval['all']) { - return true; - } - - // now we also check against the perms for contents. - if ($this->range_type === 'user') { - return $this->hasUserWriteApproval($user); - } else { - if (!count($this->write_approval)) { - return false; - } - - if ($this->write_approval['all']) { - return true; - } - - // find user in users - $users = $this->write_approval['users']->getArrayCopy(); - if (in_array($user->id, $users)) { + if ($this->permission_type === 'all' || $this->writable_all) { + if ($this->isWritable()) { return true; } + return false; + } + if ($this->permission_type === 'users') { + return in_array($user->id, json_decode($this->writable_approval)) && $this->isWritable(); + } - // find user in groups - foreach (\Statusgruppen::findMany($this->write_approval['groups']->getArrayCopy()) as $group) { - if ($group->isMember($user->id)) { - return true; + if ($this->permission_type === 'groups') { + $groups = json_decode($this->writable_approval); + foreach ($groups as $groupId) { + $group = \Statusgruppen::find($groupId); + if ($group && $group->isMember($user->id)) { + return $this->isWritable(); } } } @@ -539,26 +538,39 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac return false; } - private function hasUserWriteApproval($user): bool + private function isWritable(): bool { - if (!count($this->write_approval)) { + if ($this->writable === 'always') { + return true; + } + if ($this->writable === 'never') { + return false; + } + + return + (empty($this->writable_start_date) || $this->writable_start_date < strtotime('today')) + && (empty($this->writable_end_date) || $this->writable_end_date >= strtotime('today')); + } + + private function hasWriteContentApproval($user): bool + { + if (!count($this->content_approval)) { if ($this->isRootNode()) { return false; } - return $this->parent->hasUserWriteApproval($user); + return $this->parent->hasWriteContentApproval($user); } // find user in users - $users = $this->write_approval['users']; + $users = $this->content_approval['users']; foreach ($users as $listedUserPerm) { - // now for contents, there is an expiry date defined. if (!empty($listedUserPerm['expiry']) && strtotime($listedUserPerm['expiry']) < strtotime('today')) { if ($this->isRootNode()) { return false; } - return $this->parent->hasUserWriteApproval($user); + return $this->parent->hasWriteContentApproval($user); } - if ($listedUserPerm['id'] == $user->id) { + if ($listedUserPerm['id'] == $user->id && $listedUserPerm['write']) { return true; } } @@ -566,7 +578,7 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac if ($this->isRootNode()) { return false; } - return $this->parent->hasUserWriteApproval($user); + return $this->parent->hasWriteContentApproval($user); } /** @@ -586,30 +598,6 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac return $this->previousProgressAchieved($user); } - /** - * @return bool true if the user may read this instance in time interval - * - * @SuppressWarnings(PHPMD.Superglobals) - */ - private function releasedForReaders(StructuralElement $element): bool - { - $released = false; - if (!$element->release_date || $element->release_date <= time()) { - $released = true; - } - - if ($element->withdraw_date && $element->withdraw_date <= time()) { - $released = false; - } - - $parent_released = true; - if (!$element->isRootNode()) { - $parent_released = $this->releasedForReaders($element->parent); - } - - return $released && $parent_released; - } - /** * @param mixed $user the user to validate * @@ -839,13 +827,18 @@ SQL; /** * Copies this instance into another course oder users contents. * - * @param User $user this user will be the owner of the copy - * @param Range $parent the target where to copy this instance + * @param User $user this user will be the owner of the copy + * @param \Range $parent the target where to copy this instance * * @return StructuralElement the copy of this instance */ - public function copyToRange(User $user, string $rangeId, string $rangeType, string $purpose = ''): StructuralElement - { + public function copyToRange( + User $user, + string $rangeId, + string $rangeType, + string $purpose = '', + bool $duplicate = false + ): StructuralElement { $element = self::build([ 'parent_id' => null, 'range_id' => $rangeId, @@ -857,7 +850,19 @@ SQL; 'purpose' => $purpose ?: $this->purpose, 'position' => 0, 'payload' => $this->payload, - 'commentable' => 0 + 'commentable' => $duplicate ? $this->commentable : 0, + 'permission_type' => $duplicate ? $this->permission_type : 'all', + 'visible' => $duplicate ? $this->visible : 'always', + 'visible_all' => $duplicate ? $this->visible_all : 0, + 'visible_start_date' => $duplicate ? $this->visible_start_date : null, + 'visible_end_date' => $duplicate ? $this->visible_end_date : null, + 'visible_approval' => $duplicate ? $this->visible_approval : '', + 'writable' => $duplicate ? $this->writable : 'never', + 'writable_all' => $duplicate ? $this->writable_all : 0, + 'writable_start_date' => $duplicate ? $this->writable_start_date : null, + 'writable_end_date' => $duplicate ? $this->writable_end_date : null, + 'writable_approval' => $duplicate ? $this->writable_approval : '', + 'content_approval' => $duplicate ? $this->content_approval : '', ]); $element->store(); @@ -869,7 +874,7 @@ SQL; $this->copyContainers($user, $element); - $this->copyChildren($user, $element, $purpose); + $this->copyChildren($user, $element, $purpose, '', $duplicate); return $element; } @@ -877,15 +882,20 @@ SQL; /** * Copies this instance as a child into another structural element. * - * @param User $user this user will be the owner of the copy + * @param User $user this user will be the owner of the copy * @param StructuralElement $parent the target where to copy this instance * @param string $purpose the purpose of copying this instance * @param string $recursiveId the optional mapping id for copying child structural elements upon recursive call to this function * * @return StructuralElement the copy of this instance */ - public function copy(User $user, StructuralElement $parent, string $purpose = '', string $recursiveId = ''): StructuralElement - { + public function copy( + User $user, + StructuralElement $parent, + string $purpose = '', + string $recursiveId = '', + bool $duplicate = false + ): StructuralElement { $ancestorIds = array_column($parent->findAncestors(), 'id'); $ancestorIds[] = $parent->id; if (in_array($this->id, $ancestorIds)) { @@ -908,9 +918,19 @@ SQL; 'payload' => $this->payload, 'image_id' => $image_id, 'image_type' => $this->image_type, - 'read_approval' => $parent->read_approval, - 'write_approval' => $parent->write_approval, - 'commentable' => 0 + 'permission_type' => $duplicate ? $this->permission_type : 'all', + 'visible' => $duplicate ? $this->visible : 'always', + 'visible_all' => $duplicate ? $this->visible_all : 0, + 'visible_start_date' => $duplicate ? $this->visible_start_date : null, + 'visible_end_date' => $duplicate ? $this->visible_end_date : null, + 'visible_approval' => $duplicate ? $this->visible_approval : '', + 'writable' => $duplicate ? $this->writable : 'never', + 'writable_all' => $duplicate ? $this->writable_all : 0, + 'writable_start_date' => $duplicate ? $this->writable_start_date : null, + 'writable_end_date' => $duplicate ? $this->writable_end_date : null, + 'writable_approval' => $duplicate ? $this->writable_approval : '', + 'content_approval' => $duplicate ? $this->content_approval : '', + 'commentable' => $duplicate ? $this->commentable : 0, ]); $element->store(); @@ -1027,10 +1047,15 @@ SQL; return [$containerMap, $blockMap]; } - private function copyChildren(User $user, StructuralElement $newElement, string $purpose = '', string $recursiveId = ''): void - { + private function copyChildren( + User $user, + StructuralElement $newElement, + string $purpose = '', + string $recursiveId = '', + bool $duplicate = false + ): void { foreach ($this->children as $child) { - $child->copy($user, $newElement, $purpose, $recursiveId); + $child->copy($user, $newElement, $purpose, $recursiveId, $duplicate); } } @@ -1049,8 +1074,16 @@ SQL; 'purpose' => $this->purpose, 'position' => $parent->countChildren(), 'payload' => $this->payload, - 'read_approval' => $parent->read_approval, - 'write_approval' => $parent->write_approval, + 'permission_type'=> $parent->permission_type, + 'visible' => $parent->visible, + 'visible_start_date' => $parent->visible_start_date, + 'visible_end_date' => $parent->visible_end_date, + 'writable' => $parent->writable, + 'writable_start_date' => $parent->writable_start_date, + 'writable_end_date' => $parent->writable_end_date, + 'visible_approval' => $parent->visible_approval, + 'writable_approval' => $parent->writable_approval, + 'content_approval' => $parent->content_approval, 'commentable' => 0 ]); diff --git a/lib/models/Courseware/Unit.php b/lib/models/Courseware/Unit.php index 2a38a291d40742cd1aa0bcd705a063c8495d7136..86013e368db9be6c0f2578b1dee8dba0d965ddf2 100644 --- a/lib/models/Courseware/Unit.php +++ b/lib/models/Courseware/Unit.php @@ -20,8 +20,18 @@ use User; * @property string $content_type database column * @property int $public database column * @property string|null $creator_id database column - * @property int|null $release_date database column - * @property int|null $withdraw_date database column + * @property string $permission_scope database column + * @property string $permission_type database column + * @property string $visible database column + * @property bool $visible_all database column + * @property int|null $visible_start_date database column + * @property int|null $visible_end_date database column + * @property string $writable database column + * @property bool $writable_all database column + * @property int|null $writable_start_date database column + * @property int|null $writable_end_date database column + * @property \JSONArrayObject $writable_approval database column + * @property \JSONArrayObject $visible_approval database column * @property \JSONArrayObject $config database column * @property int $mkdate database column * @property int $chdate database column @@ -38,6 +48,8 @@ class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange $config['db_table'] = 'cw_units'; $config['serialized_fields']['config'] = JSONArrayObject::class; + $config['serialized_fields']['visible_approval'] = JSONArrayObject::class; + $config['serialized_fields']['writable_approval'] = JSONArrayObject::class; $config['has_one']['structural_element'] = [ 'class_name' => StructuralElement::class, @@ -45,7 +57,7 @@ class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange 'on_delete' => 'delete', ]; $config['belongs_to']['course'] = [ - 'class_name' => \Course::class, + 'class_name' => \Course::class, 'foreign_key' => 'range_id', 'assoc_foreign_key' => 'seminar_id', ]; @@ -75,26 +87,116 @@ class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange return self::findBySQL('range_id = ? AND range_type = ?', [$course->id, 'course']); } - public static function findUsersUnits(\User $user): array + public static function findUsersUnits(User $user): array { return self::findBySQL('range_id = ? AND range_type = ?', [$user->id, 'user']); } - public function canRead(\User $user): bool + public function canRead(User $user): bool { - return $this->structural_element->canRead($user); + if ($this->canEdit($user) || $this->canEditContent($user)) { + return true; + } + if ($this->permission_scope === 'unit') { + if ($this->permission_type === 'all' || $this->visible_all) { + if ($this->isVisible()) { + return true; + } + return false; + } + if ($this->permission_type === 'users') { + return in_array($user->id, json_decode($this->visible_approval)) && $this->isVisible(); + } + if ($this->permission_type === 'groups') { + $groups = json_decode($this->visible_approval); + foreach ($groups as $groupId) { + $group = \Statusgruppen::find($groupId); + if ($group && $group->isMember($user->id)) { + return $this->isVisible(); + } + } + } + } + + return true; + } + + private function isVisible(): bool + { + if ($this->visible === 'always') { + return true; + } + if ($this->visible === 'never') { + return false; + } + + return + (empty($this->visible_start_date) || $this->visible_start_date < strtotime('today')) + && (empty($this->visible_end_date) || $this->visible_end_date >= strtotime('today')); + } + + public function canEdit(User $user): bool + { + if ($user->id === $this->range_id) { + return true; + } + + return $GLOBALS['perm']->have_perm('root', $user->id) || $GLOBALS['perm']->have_studip_perm('tutor', $this->range_id, $user->id); } - public function canEdit(\User $user): bool + public function canEditContent(User $user): bool { - return $this->structural_element->canEdit($user);; + if ($this->canEdit($user)) { + return true; + } + if ($this->permission_scope === 'unit') { + if ($this->permission_type === 'all' || $this->writable_all) { + if ($this->isWritable()) { + return true; + } + return false; + } + if ($this->permission_type === 'users') { + return in_array($user->id, json_decode($this->writable_approval)) && $this->isWritable(); + } + if ($this->permission_type === 'groups') { + $groups = json_decode($this->writable_approval); + foreach ($groups as $groupId) { + $group = \Statusgruppen::find($groupId); + if ($group && $group->isMember($user->id)) { + return $this->isWritable(); + } + } + } + } + + return false; } - public function copy(\User $user, string $rangeId, string $rangeType, array $modified = null): Unit + private function isWritable(): bool { + if ($this->writable === 'always') { + return true; + } + if ($this->writable === 'never') { + return false; + } + + return + (empty($this->writable_start_date) || $this->writable_start_date < strtotime('today')) + && (empty($this->writable_end_date) || $this->writable_end_date >= strtotime('today')); + } + + public function copy( + User $user, + string $rangeId, + string $rangeType, + array $modified = null, + bool $duplicate = false + ): Unit { $sourceUnitElement = $this->structural_element; - $newElement = $sourceUnitElement->copyToRange($user, $rangeId, $rangeType); + $newElement = $sourceUnitElement->copyToRange($user, $rangeId, $rangeType, '', $duplicate); if ($modified !== null) { $newElement->title = $modified['title'] ?? $newElement->title; @@ -108,10 +210,21 @@ class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange 'range_type' => $rangeType, 'structural_element_id' => $newElement->id, 'content_type' => 'courseware', + 'position' => $this->getNewPosition($rangeId), 'creator_id' => $user->id, 'public' => '', - 'release_date' => null, - 'withdraw_date' => null, + 'permission_scope' => $duplicate ? $this->permission_scope : 'unit', + 'permission_type' => $duplicate ? $this->permission_type : 'all', + 'visible' => $duplicate ? $this->visible : 'always', + 'visible_all' => $duplicate ? $this->visible_all : 0, + 'visible_start_date' => $duplicate ? $this->visible_start_date : null, + 'visible_end_date' => $duplicate ? $this->visible_end_date : null, + 'visible_approval' => $duplicate ? $this->visible_approval : '', + 'writable' => $duplicate ? $this->writable : 'never', + 'writable_all' => $duplicate ? $this->writable_all : 0, + 'writable_start_date' => $duplicate ? $this->writable_start_date : null, + 'writable_end_date' => $duplicate ? $this->writable_end_date : null, + 'writable_approval' => $duplicate ? $this->writable_approval : '', ]); $newUnit->store(); @@ -133,7 +246,7 @@ class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange if ($units) { $storage->addTabularData(_('Courseware Lernmaterialien'), 'cw_units', $units); } - + } public static function getNewPosition($range_id): int @@ -148,16 +261,18 @@ class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange } $db = \DBManager::get(); - $stmt = $db->prepare(sprintf( - 'UPDATE + $stmt = $db->prepare( + sprintf( + 'UPDATE %s SET position = position - 1 WHERE range_id = :range_id AND position > :position', - 'cw_units' - )); + 'cw_units' + ) + ); $stmt->bindValue(':range_id', $this->range_id); $stmt->bindValue(':position', $this->position); $stmt->execute(); @@ -173,7 +288,8 @@ class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange position = FIND_IN_SET(id, ?) - 1 WHERE range_id = ?', - 'cw_units'); + 'cw_units' + ); $args = array(join(',', $positions), $range->id); $stmt = $db->prepare($query); $stmt->execute($args); @@ -226,7 +342,7 @@ class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange public function getRangeUrl(): string { if ($this->structural_element->range_type === 'user') { - return 'contents/courseware/'; + return 'contents/courseware/'; } return 'course/courseware/' . '?cid=' . $this->range_id; @@ -234,7 +350,7 @@ class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange public function isRangeAccessible(string $user_id = null): bool { - $user = \User::find($user_id); + $user = \User::find($user_id); if ($user) { return $this->canRead($user); } @@ -249,4 +365,9 @@ class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange [$this->id, self::class] ); } + + public function getRange() + { + return $this->range_type::find($this->range_id); + } } diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index c555b6dbeeee57aa1c155f31a70ac6cbbda78b9c..8c9d87f64cca54d28e3fbe3eac2dfd3a587a8cd1 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -26,7 +26,9 @@ @import './courseware/layouts/companion.scss'; @import './courseware/layouts/import-zip.scss'; @import './courseware/layouts/input-file.scss'; +@import './courseware/layouts/permissions.scss'; @import './courseware/layouts/progress.scss'; +@import './courseware/layouts/radioset.scss'; @import './courseware/layouts/ribbon.scss'; @import './courseware/layouts/tabs.scss'; @import './courseware/layouts/talk-bubble.scss'; diff --git a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss index baed50329634685aa771aaf808dfda2b23c8e5d1..948339ce94651c0668f218b5503fd363ba6b60c1 100644 --- a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss +++ b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss @@ -103,67 +103,3 @@ form.cw-container-dialog-edit-form { max-width: 200px; } } - -.cw-radioset { - display: flex; - flex-direction: row; - justify-content: center; - margin-bottom: 1em; - .cw-radioset-box { - width: 128px; - height: 128px; - text-align: center; - margin-right: 16px; - border: solid thin var(--content-color-40); - &.selected { - border-color: var(--base-color); - background-color: var(--content-color-20); - } - &:last-child { - margin-right: 0; - } - label { - height: 100%; - width: 100%; - margin: 0; - cursor: pointer; - .label-icon { - background-position: center 8px; - background-repeat: no-repeat; - height: 64px; - padding: 8px; - &.accordion { - @include background-icon(block-accordion, clickable, 64); - } - &.list { - @include background-icon(view-list, clickable, 64); - } - &.tabs { - @include background-icon(block-tabs, clickable, 64); - } - &.full { - @include background-icon(column-full, clickable, 64); - } - &.half { - @include background-icon(column-half, clickable, 64); - } - &.half-center { - @include background-icon(column-half-centered, clickable, 64); - } - } - - } - input[type=radio] { - position: absolute; - opacity: 0; - width: 0; - - &:focus + label { - outline-color: Highlight; - outline-color: -webkit-focus-ring-color; - outline-style: auto; - outline-width: 1px; - } - } - } -} diff --git a/resources/assets/stylesheets/scss/courseware/layouts/permissions.scss b/resources/assets/stylesheets/scss/courseware/layouts/permissions.scss new file mode 100644 index 0000000000000000000000000000000000000000..fa84a1f4a145042332a353ffc3a5c949170092d5 --- /dev/null +++ b/resources/assets/stylesheets/scss/courseware/layouts/permissions.scss @@ -0,0 +1,32 @@ +.cw-permissions-form-wrapper { + display: flex; + max-width: 840px; + flex-direction: row; + gap: 1em; + + .cw-form-selects { + flex-grow: 1; + + .cw-form-selects-row { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 1em; + + label { + flex-grow: 1; + text-indent: 0; + + select { + min-width: 9em; + } + } + } + } + + .permission-table { + img { + padding: 0 4px; + } + } +} \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware/layouts/radioset.scss b/resources/assets/stylesheets/scss/courseware/layouts/radioset.scss new file mode 100644 index 0000000000000000000000000000000000000000..a65a189b3a1eac6ad2679136f415602e75a3584b --- /dev/null +++ b/resources/assets/stylesheets/scss/courseware/layouts/radioset.scss @@ -0,0 +1,88 @@ +$radio-icons: ( + accordion: block-accordion, + list: view-list, + tabs: block-tabs, + full: column-full, + half: column-half, + half-center: column-half-centered, + all: group3, + users: person, + groups: group2 +); + +.cw-radioset { + display: flex; + flex-direction: row; + justify-content: center; + margin-bottom: 1em; + + .cw-radioset-box { + width: 128px; + height: 128px; + text-align: center; + margin-right: 8px; + border: solid thin var(--content-color-40); + label { + height: 100%; + width: 100%; + margin: 0; + cursor: pointer; + @include background-icon(radiobutton-unchecked, clickable); + background-position: center 104px; + background-repeat: no-repeat; + + .label-text { + height: 48px; + max-width: 80%; + overflow: hidden; + display: grid; + align-items: center; + margin: 0 auto; + } + + .label-icon { + background-position: center 8px; + background-repeat: no-repeat; + height: 48px; + padding: 4px; + + @each $type, $icon in $radio-icons { + &.#{$type} { + @include background-icon(#{$icon}, clickable, 48); + } + } + } + + } + input[type=radio] { + position: absolute; + opacity: 0; + width: 0; + + &:focus + label { + outline-color: Highlight; + outline-color: -webkit-focus-ring-color; + outline-style: auto; + outline-width: 1px; + } + } + + &.selected { + border-color: var(--base-color); + background-color: var(--content-color-20); + label { + @include background-icon(check-circle, clickable); + .label-icon { + @each $type, $icon in $radio-icons { + &.#{$type} { + @include background-icon(#{$icon}, info, 48); + } + } + } + } + } + &:last-child { + margin-right: 0; + } + } +} \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware/layouts/tile.scss b/resources/assets/stylesheets/scss/courseware/layouts/tile.scss index a5632442a3305989bb3bf78630d0fe9605e55021..7b56857b0d4b0519cdb32cbd8f1a953e93a9df6c 100644 --- a/resources/assets/stylesheets/scss/courseware/layouts/tile.scss +++ b/resources/assets/stylesheets/scss/courseware/layouts/tile.scss @@ -55,6 +55,19 @@ text-align: right; } + .overlay-text-bottom { + padding: 6px 7px; + margin: 146px -33px 4px 4px; + background-color: rgba(255, 255, 255, 0.8); + width: fit-content; + max-width: 100%; + height: 1.25em; + overflow: hidden; + text-overflow: ellipsis; + float: right; + text-align: right; + } + .overlay-action-menu { padding: 0; margin: 0.25em; @@ -89,12 +102,8 @@ background-repeat: no-repeat; background-position: 0 0; - @each $type, $icon in $element-icons { - &.description-icon-#{$type} { - width: 212px; - padding-left: 28px; - @include background-icon(#{$icon}, info_alt, 22); - } + .title-icon { + vertical-align: bottom; } } @@ -126,10 +135,10 @@ .description-text-wrapper { overflow: hidden; - height: 10em; + height: 7em; margin-top: 4px; display: -webkit-box; - -webkit-line-clamp: 7; + -webkit-line-clamp: 5; -webkit-box-orient: vertical; p { text-align: left; @@ -137,16 +146,16 @@ } footer { - width: 242px; - margin-top: 8px; + display: table-cell; + height: 5em; color: var(--white); - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - display: flex; - align-items: center; - justify-content: space-between; + vertical-align: bottom; + p { + margin: 0; + } img { vertical-align: text-bottom; } diff --git a/resources/assets/stylesheets/scss/dialog.scss b/resources/assets/stylesheets/scss/dialog.scss index f45dc9b4e0cb72ba4168a5d83f82b7bcb81f2b0c..bd8ad030e68f2739b58b43ece6767c9b18f3016f 100644 --- a/resources/assets/stylesheets/scss/dialog.scss +++ b/resources/assets/stylesheets/scss/dialog.scss @@ -320,7 +320,7 @@ v u e d i a l o g display: flex; justify-content: center; align-items: center; - z-index: 3001; + z-index: 1001; } .studip-dialog-body { position: absolute; diff --git a/resources/vue/components/courseware/CoursewareContentPermissions.vue b/resources/vue/components/courseware/CoursewareContentPermissions.vue index 60f09644471f5e11af7c7d949b6ccd0dd1812b12..841fc8ffd8a5228859208afeaf22f1abf6591736 100644 --- a/resources/vue/components/courseware/CoursewareContentPermissions.vue +++ b/resources/vue/components/courseware/CoursewareContentPermissions.vue @@ -8,7 +8,7 @@ </studip-message-box> <table class="default"> <caption> - <translate>Personen</translate> + {{ $gettext('Personen') }} </caption> <colgroup> <col style="width:35%"> @@ -19,17 +19,17 @@ </colgroup> <thead> <tr> - <th><translate>Name</translate></th> - <th><translate>Leserechte</translate></th> - <th><translate>Lese- und Schreibrechte</translate></th> - <th><translate>Ablaufdatum</translate></th> - <th class="actions"><translate>Aktion</translate></th> + <th>{{ $gettext('Name') }}</th> + <th>{{ $gettext('Leserechte') }}</th> + <th>{{ $gettext('Lese- und Schreibrechte') }}</th> + <th>{{ $gettext('Ablaufdatum') }}</th> + <th class="actions">{{ $gettext('Aktion') }}</th> </tr> </thead> <tbody> <tr v-if="listEmpty" class="empty"> <td colspan="5"> - <translate>Es wurden noch keine Freigaben erteilt</translate> + {{ $gettext('Es wurden noch keine Freigaben erteilt') }} </td> </tr> <tr v-for="(user_perm, index) of userPermsList" :key="index"> @@ -69,7 +69,7 @@ :min="minDate" :id="`${user_perm.id}_expiry`" v-model="userPermsList[index]['expiry']" - @change="refreshReadWriteApproval" + @change="refreshContentApproval" /> </td> <td class="actions"> @@ -87,7 +87,7 @@ <td colspan="5"> <span class="multibuttons"> <button class="button add cw-add-persons" @click.prevent="showAddMultiPersonDialog = true"> - <translate>Personen hinzufügen</translate> + {{ $gettext('Personen hinzufügen') }} </button> <button class="button" @@ -95,7 +95,7 @@ :disabled="listEmpty" @click.prevent="setAllPerms('read')" > - <translate>Allen Leserechte geben</translate> + {{ $gettext('Allen Leserechte geben') }} </button> <button class="button" @@ -103,7 +103,7 @@ :disabled="listEmpty" @click.prevent="setAllPerms('write')" > - <translate>Allen Lese- und Schreibrechte geben</translate> + {{ $gettext('Allen Lese- und Schreibrechte geben') }} </button> </span> </td> @@ -149,10 +149,7 @@ export default { showAddMultiPersonDialog: null, userPermsList: [], selectedUsers:[], - userPermsReadAll: false, - userPermsWriteAll: false, - userPermsReadUsers: [], - userPermsWriteUsers: [], + contentApprovalUsers: [], message: false, showDeleteDialog: false, deleteUserPermIndex: -1 @@ -160,16 +157,6 @@ export default { }, mounted() { - if (this.element.attributes['read-approval'].all !== undefined) { - this.userPermsReadAll = this.element.attributes['read-approval'].all; - } else { - this.userPermsReadAll = false; - } - if (this.element.attributes['write-approval'].all !== undefined) { - this.userPermsWriteAll = this.element.attributes['write-approval'].all; - } else { - this.userPermsWriteAll = false; - } this.initUserPermsList(); }, @@ -182,19 +169,9 @@ export default { return this.userPermsList.length === 0; }, - readApproval() { - return { - all: this.userPermsReadAll, - users: this.userPermsReadUsers, - groups: [] - }; - }, - - writeApproval() { + contentApproval() { return { - all: this.userPermsWriteAll, - users: this.userPermsWriteUsers, - groups: [] + users: this.contentApprovalUsers, }; }, @@ -231,22 +208,17 @@ export default { async initUserPermsList() { - if (this.element.attributes['read-approval'].users !== undefined) { - this.userPermsReadUsers = this.element.attributes['read-approval'].users; - } - - if (this.element.attributes['write-approval'].users !== undefined) { - this.userPermsWriteUsers = this.element.attributes['write-approval'].users; + if (this.element.attributes['content-approval'].users !== undefined) { + this.contentApprovalUsers = this.element.attributes['content-approval'].users; } /* eslint-disable no-await-in-loop */ - for (const user_perm_obj of this.userPermsReadUsers) { + for (const user_perm_obj of this.contentApprovalUsers) { let userObj = await this.getUser(user_perm_obj.id); - let writePerm = this.userPermsWriteUsers.some(user_write_perm => user_write_perm.id === user_perm_obj.id) ? true : false; this.userPermsList.push({ 'id' : user_perm_obj.id, 'read': user_perm_obj.read, - 'write': writePerm, + 'write': user_perm_obj.write, 'expiry': user_perm_obj.expiry ? new Date(user_perm_obj.expiry).toISOString().split('T')[0] : '', 'formatted-name': userObj.attributes['formatted-name'], 'username': userObj.attributes['username'], @@ -272,7 +244,7 @@ export default { 'username': selected_user.username, }; this.userPermsList.push(newUserPerm); - this.refreshReadWriteApproval(); + this.refreshContentApproval(); } else { duplicatedUsers.push(selected_user); } @@ -305,7 +277,7 @@ export default { performDeleteUserPerm() { if (this.deleteUserPermIndex !== -1) { this.userPermsList.splice(this.deleteUserPermIndex, 1); - this.refreshReadWriteApproval(); + this.refreshContentApproval(); } this.clearDeleteUserPerm(); }, @@ -328,7 +300,7 @@ export default { this.userPermsList[index]['read'] = read; this.userPermsList[index]['write'] = write; - this.refreshReadWriteApproval(); + this.refreshContentApproval(); }, setAllPerms(permtype) { @@ -344,54 +316,21 @@ export default { return true; }); - this.refreshReadWriteApproval(); + this.refreshContentApproval(); }, - refreshReadWriteApproval() { - this.refreshReadApproval(); - this.refreshWriteApproval(); - }, - - refreshReadApproval() { - this.userPermsReadUsers = []; + refreshContentApproval() { + this.contentApprovalUsers = []; for (const user_perm_obj of this.userPermsList) { let readRight = user_perm_obj.write ? true : user_perm_obj.read; - this.userPermsReadUsers.push({ + this.contentApprovalUsers.push({ 'id': user_perm_obj.id, 'read': readRight, 'write': user_perm_obj.write, 'expiry': user_perm_obj.expiry ? new Date(user_perm_obj.expiry).toISOString() : '' }); } - this.$emit('updateReadApproval', this.readApproval); - }, - - refreshWriteApproval() { - this.userPermsWriteUsers = []; - for (const user_perm_obj of this.userPermsList) { - if (user_perm_obj.write) { - this.userPermsWriteUsers.push({ - 'id': user_perm_obj.id, - 'expiry': user_perm_obj.expiry ? new Date(user_perm_obj.expiry).toISOString() : '' - }); - } - } - this.$emit('updateWriteApproval', this.writeApproval); - } - }, - - watch: { - userPermsReadAll(newVal, oldVal) { - this.$emit('updateReadApproval', this.readApproval); - if (newVal === true) { - this.userPermsWriteAll = false; - } - }, - userPermsWriteAll(newVal, oldVal) { - this.$emit('updateWriteApproval', this.writeApproval); - if (newVal === true) { - this.userPermsReadAll = false; - } + this.$emit('updateContentApproval', this.contentApproval); }, }, }; diff --git a/resources/vue/components/courseware/CoursewareContentShared.vue b/resources/vue/components/courseware/CoursewareContentShared.vue index 8eb9bf6104a1e7c4658974b33e6382291aaaa6b4..0bdc4b5cfd2228596a3cae3af521f18c3a3a1f58 100644 --- a/resources/vue/components/courseware/CoursewareContentShared.vue +++ b/resources/vue/components/courseware/CoursewareContentShared.vue @@ -65,8 +65,7 @@ <template v-slot:dialogContent> <courseware-content-permissions :element="selectedElement" - @updateReadApproval="updateReadApproval" - @updateWriteApproval="updateWriteApproval" + @updateContentApproval="updateContentApproval" /> </template> </studip-dialog> @@ -123,11 +122,8 @@ export default { getElementUrl(element) { return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/courseware#/structural_element/' + element.id; }, - updateReadApproval(approval) { - this.selectedElement.attributes['read-approval'] = approval; - }, - updateWriteApproval(approval) { - this.selectedElement.attributes['write-approval'] = approval; + updateContentApproval(approval) { + this.selectedElement.attributes['content-approval'] = approval; }, displayEditReleases(element) { this.selectedElement = element; diff --git a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue index 427a93092c6ceb56248335368c76d16dba5a0976..7f4a882e84feb29ef6ad30ae739c4a91ad744f0a 100644 --- a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue +++ b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue @@ -65,7 +65,7 @@ @close="closeChange" @confirm="storeChange" height="520" - width="480" + width="440" > <template v-slot:dialogContent> <form class="default" @submit.prevent=""> @@ -81,7 +81,7 @@ <input type="radio" :id="'type-' + container.type" :value="container.type" v-model="changeType" name="container-type"/> <label :for="'type-' + container.type" > <div class="label-icon" :class="[container.type, container.type === changeType ? 'selected' : '']"></div> - <p>{{ container.title }}</p> + <div class="label-text"><span>{{ container.title }}</span></div> </label> </div> @@ -99,7 +99,7 @@ <input type="radio" :id="'change-style-' + style.colspan" :value="style.colspan" v-model="changeStyle" name="change-container-style"/> <label :for="'change-style-' + style.colspan"> <div class="label-icon" :class="[style.colspan, style.colspan === changeStyle ? 'selected' : '']"></div> - <p>{{ style.title }}</p> + <div class="label-text"><span>{{ style.title }}</span></div> </label> </div> </div> diff --git a/resources/vue/components/courseware/layouts/CoursewareTile.vue b/resources/vue/components/courseware/layouts/CoursewareTile.vue index 1b37511eb3d87a358a963e69c458536ca8fd9bab..d2f53c3535698707384e7bbccf0e403a408afdef 100644 --- a/resources/vue/components/courseware/layouts/CoursewareTile.vue +++ b/resources/vue/components/courseware/layouts/CoursewareTile.vue @@ -11,11 +11,14 @@ :id="handleId" @keydown="$emit('handle-keydown', $event)" ></div> + <div class="overlay-action-menu" v-if="hasImageOverlayWithActionMenu"> + <slot name="image-overlay-with-action-menu"></slot> + </div> <div class="overlay-text" v-if="hasImageOverlay"> <slot name="image-overlay"></slot> </div> - <div class="overlay-action-menu" v-if="hasImageOverlayWithActionMenu"> - <slot name="image-overlay-with-action-menu"></slot> + <div class="overlay-text-bottom" v-if="hasImageOverlayBottom"> + <slot name="image-overlay-bottom"></slot> </div> </div> <component @@ -24,7 +27,8 @@ :title="descriptionTitle" class="description" > - <header :class="[icon ? 'description-icon-' + icon : '']"> + <header> + <studip-icon v-if="icon" class="title-icon" :shape="icon" role="info_alt" :size="24" /> {{ title }} </header> <div v-if="displayProgress" :title="progressTitle" class="progress-wrapper"> @@ -133,6 +137,9 @@ export default { hasImageOverlay() { return this.$slots['image-overlay'] !== undefined; }, + hasImageOverlayBottom() { + return this.$slots['image-overlay-bottom'] !== undefined; + }, hasImageOverlayWithActionMenu() { return this.$slots['image-overlay-with-action-menu'] !== undefined; }, diff --git a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue index 1aa88028ac251b5fb9300317c0fd2ee79073a280..ee820c4a1754a63878b61c7c671b5a5d0e163c85 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue @@ -24,7 +24,7 @@ class="cw-root-content-description-img" v-model="identImage" :baseColor="bgColorHex" - :pattern="structuralElement.attributes.title" + :pattern="structuralElement.attributes?.title ?? '-'" /> <studip-ident-image v-model="identBgImage" @@ -32,13 +32,13 @@ :width="4380" :height="withTOC ? 1200 : 1920" :baseColor="bgColorHex" - :pattern="structuralElement.attributes.title" + :pattern="structuralElement.attributes?.title ?? '-'" /> </template> <div class="cw-root-content-description-text"> - <h1>{{ structuralElement.attributes.title }}</h1> + <h1>{{ structuralElement.attributes?.title ?? '-' }}</h1> <p> - {{ structuralElement.attributes.payload.description }} + {{ structuralElement.attributes?.payload?.description ?? '-' }} </p> </div> </section> @@ -134,6 +134,7 @@ export default { const color = this.mixinColors.find((c) => { return c.class === elementColor; }); + return color.hex; }, bgColor() { diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue index d423eacf6956aacc4056bc30ce1e02f2035f7675..2b7cdbc610b571c5b25442f6d6d550c5ce0e0841 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue @@ -7,16 +7,28 @@ class="cw-structural-element" > <div v-if="structuralElement" class="cw-structural-element-content"> - <courseware-ribbon :canEdit="canEdit && canAddElements" :isContentBar="true" @blockAdded="updateContainerList"> + <courseware-ribbon + :canEdit="canEdit && canAddElements" + :isContentBar="true" + @blockAdded="updateContainerList" + > <template #buttons> <router-link v-if="prevElement" :to="'/structural_element/' + prevElement.id"> - <div class="cw-ribbon-button cw-ribbon-button-prev" :title="textRibbon.perv" /> + <div class="cw-ribbon-button cw-ribbon-button-prev" :title="$gettext('zurück')" /> </router-link> - <div v-else class="cw-ribbon-button cw-ribbon-button-prev-disabled" :title="$gettext('Keine vorherige Seite')"/> + <div + v-else + class="cw-ribbon-button cw-ribbon-button-prev-disabled" + :title="$gettext('Keine vorherige Seite')" + /> <router-link v-if="nextElement" :to="'/structural_element/' + nextElement.id"> - <div class="cw-ribbon-button cw-ribbon-button-next" :title="textRibbon.next" /> + <div class="cw-ribbon-button cw-ribbon-button-next" :title="$gettext('weiter')" /> </router-link> - <div v-else class="cw-ribbon-button cw-ribbon-button-next-disabled" :title="$gettext('Keine nächste Seite')"/> + <div + v-else + class="cw-ribbon-button cw-ribbon-button-next-disabled" + :title="$gettext('Keine nächste Seite')" + /> </template> <template #breadcrumbList> <li @@ -26,14 +38,16 @@ class="cw-ribbon-breadcrumb-item" > <span> - <router-link :to="'/structural_element/' + ancestor.id">{{ ancestor.attributes.title || "–" }}</router-link> + <router-link :to="'/structural_element/' + ancestor.id">{{ + ancestor.attributes.title || '–' + }}</router-link> </span> </li> <li class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current" :title="structuralElement.attributes.title" > - <span>{{ structuralElement.attributes.title || "–" }}</span> + <span>{{ structuralElement.attributes.title || '–' }}</span> <span v-if="isTask">[ {{ solverName }} ]</span> <template v-if="!userIsTeacher && inCourse"> <studip-icon @@ -44,10 +58,11 @@ /> <span v-else - :title="$gettextInterpolate( - $gettext('Fortschritt: %{progress} %'), - {progress: elementProgress} - )" + :title=" + $gettextInterpolate($gettext('Fortschritt: %{progress} %'), { + progress: elementProgress, + }) + " > ({{ elementProgress }} %) </span> @@ -58,11 +73,11 @@ :size="16" :role="hasFeedbackAverage ? 'status-yellow' : 'inactive'" :title=" - hasFeedbackAverage ? - $gettextInterpolate($gettext('Seite wurde mit %{avg} Sternen bewertet'), { - avg: feedbackAverage, - }) : - $gettext('Seite wurde noch nicht bewertet') + hasFeedbackAverage + ? $gettextInterpolate($gettext('Seite wurde mit %{avg} Sternen bewertet'), { + avg: feedbackAverage, + }) + : $gettext('Seite wurde noch nicht bewertet') " @click="menuAction('showFeedback')" /> @@ -97,6 +112,7 @@ @showFeedback="menuAction('showFeedback')" @showFeedbackCreate="menuAction('showFeedbackCreate')" @showNote="menuAction('showNote')" + @showPermissions="menuAction('showPermissions')" /> </template> </courseware-ribbon> @@ -126,7 +142,14 @@ /> <courseware-companion-box v-if="blockedByAnotherUser" - :msgCompanion="$gettextInterpolate($gettext('Die Einstellungen dieser Seite werden im Moment von %{blockingUserName} bearbeitet.'), {blockingUserName: blockingUserName})" + :msgCompanion=" + $gettextInterpolate( + $gettext( + 'Die Einstellungen dieser Seite werden im Moment von %{blockingUserName} bearbeitet.' + ), + { blockingUserName: blockingUserName } + ) + " mood="pointing" > <template #companionActions> @@ -142,7 +165,11 @@ /> </div> - <courseware-root-content v-if="showRootLayout" :structuralElement="currentElement" :canEdit="canEdit" /> + <courseware-root-content + v-if="showRootLayout" + :structuralElement="currentElement" + :canEdit="canEdit" + /> <div v-if="canVisit && (!canEdit || hideEditLayout ) && !isLink && !hideRootContent" @@ -172,7 +199,14 @@ > <div v-if="canEdit" class="cw-companion-box-wrapper"> <courseware-companion-box - :msgCompanion="$gettextInterpolate($gettext('Dieser Inhalt ist aus den persönlichen Lernmaterialien von %{ ownerName } verlinkt und kann nur dort bearbeitet werden.'), { ownerName: ownerName })" + :msgCompanion=" + $gettextInterpolate( + $gettext( + 'Dieser Inhalt ist aus den persönlichen Lernmaterialien von %{ ownerName } verlinkt und kann nur dort bearbeitet werden.' + ), + { ownerName: ownerName } + ) + " mood="pointing" /> </div> @@ -191,7 +225,7 @@ <template v-if="!processing"> <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span> <span id="operation" class="assistive-text"> - {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}} + {{ $gettext('Drücken Sie die Leertaste, um neu anzuordnen.') }} </span> <draggable class="cw-structural-element-list" @@ -225,12 +259,17 @@ :isTeacher="userIsTeacher" class="cw-container-item" ref="containers" - :class="{ 'cw-container-item-selected': keyboardSelected === container.id}" + :class="{ + 'cw-container-item-selected': keyboardSelected === container.id, + }" /> </li> </draggable> </template> - <studip-progress-indicator v-if="processing" :description="$gettext('Vorgang wird bearbeitet...')" /> + <studip-progress-indicator + v-if="processing" + :description="$gettext('Vorgang wird bearbeitet...')" + /> </div> </div> <courseware-toolbar v-if="canVisit && canEdit && !isLink" /> @@ -246,323 +285,16 @@ :open="false" > <template #content> - <courseware-structural-element-comments - :structuralElement="structuralElement" - /> + <courseware-structural-element-comments :structuralElement="structuralElement" /> </template> </courseware-call-to-action-box> </div> - <studip-dialog - v-if="showEditDialog" - :title="textEdit.title" - :confirmText="textEdit.confirm" - confirmClass="accept" - :closeText="textEdit.close" - closeClass="cancel" - height="500" - :width="inContent ? '720' : '500'" - class="studip-dialog-with-tab" - @close="closeEditDialog" - @confirm="storeCurrentElement" - > - <template v-slot:dialogContent> - <courseware-tabs class="cw-tab-in-dialog"> - <courseware-tab :name="textEdit.basic" :selected="true" :index="0"> - <form class="default" @submit.prevent=""> - <label> - <translate>Titel</translate> - <input type="text" v-model="currentElement.attributes.title" /> - </label> - <label> - <translate>Beschreibung</translate> - <textarea - v-model="currentElement.attributes.payload.description" - class="cw-structural-element-description" - /> - </label> - </form> - </courseware-tab> - <courseware-tab :name="textEdit.meta" :index="1"> - <form class="default" @submit.prevent=""> - <label> - <translate>Farbe</translate> - <studip-select - v-model="currentElement.attributes.payload.color" - :options="colors" - :reduce="(color) => color.class" - label="class" - class="cw-vs-select" - > - <template #open-indicator="selectAttributes"> - <span v-bind="selectAttributes" - ><studip-icon shape="arr_1down" :size="10" - /></span> - </template> - <template #no-options> - <translate>Es steht keine Auswahl zur Verfügung</translate>. - </template> - <template #selected-option="{ name, hex }"> - <span class="vs__option-color" :style="{ 'background-color': hex }"></span - ><span>{{ name }}</span> - </template> - <template #option="{ name, hex }"> - <span class="vs__option-color" :style="{ 'background-color': hex }"></span - ><span>{{ name }}</span> - </template> - </studip-select> - </label> - <label> - <translate>Art des Lernmaterials</translate> - <select v-model="currentElement.attributes.purpose"> - <option value="content"><translate>Inhalt</translate></option> - <option v-if="!inCourse" value="template"><translate>Aufgabenvorlage</translate></option> - <option value="oer"><translate>OER-Material</translate></option> - <option value="portfolio"><translate>ePortfolio</translate></option> - <option value="draft"><translate>Entwurf</translate></option> - <option value="other"><translate>Sonstiges</translate></option> - </select> - </label> - <template v-if="currentElement.attributes.purpose === 'oer'"> - <label> - <translate>Lizenztyp</translate> - <select v-model="currentElement.attributes.payload.license_type"> - <option v-for="license in licenses" :key="license.id" :value="license.id"> - {{ license.name }} - </option> - </select> - </label> - <label> - <translate>Geschätzter zeitlicher Aufwand</translate> - <input type="text" v-model="currentElement.attributes.payload.required_time" /> - </label> - <label> - <translate>Niveau</translate><br /> - <translate>von</translate> - <select v-model="currentElement.attributes.payload.difficulty_start"> - <option value="">-</option> - <option - v-for="difficulty_start in 12" - :key="difficulty_start" - :value="difficulty_start" - > - {{ difficulty_start }} - </option> - </select> - <translate>bis</translate> - <select v-model="currentElement.attributes.payload.difficulty_end"> - <option value="">-</option> - <option - v-for="difficulty_end in 12" - :key="difficulty_end" - :value="difficulty_end" - > - {{ difficulty_end }} - </option> - </select> - </label> - </template> - </form> - </courseware-tab> - <courseware-tab :name="textEdit.image" :index="2"> - <form class="default" @submit.prevent=""> - <template v-if="hasImage"> - <img - :src="image" - class="cw-structural-element-image-preview" - :alt="$gettext('Vorschaubild')" - /> - <label> - <button class="button" @click="deleteImage" v-translate>Bild löschen</button> - </label> - </template> - - <div v-else class="cw-structural-element-image-preview-placeholder"></div> - - <div v-if="uploadFileError" class="messagebox messagebox_error"> - {{ uploadFileError }} - </div> - - <div v-show="!hasImage"> - <label> - {{ $gettext('Bild hochladen') }} - <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" /> - </label> - {{ $gettext('oder') }} - <br> - <button class="button" type="button" @click="showStockImageSelector = true"> - {{ $gettext('Aus dem Bilderpool auswählen') }} - </button> - <StockImageSelector v-if="showStockImageSelector" @close="showStockImageSelector = false" @select="onSelectStockImage" /> - </div> - </form> - </courseware-tab> - <courseware-tab v-if="(inCourse && !isTask) || inContent" :name="textEdit.approval" :index="3"> - <courseware-structural-element-permissions - v-if="inCourse" - :element="currentElement" - @updateReadApproval="updateReadApproval" - @updateWriteApproval="updateWriteApproval" - /> - <courseware-content-permissions - v-if="inContent" - :element="currentElement" - @updateReadApproval="updateReadApproval" - @updateWriteApproval="updateWriteApproval" - /> - </courseware-tab> - <courseware-tab v-if="inCourse && !isTask" :name="textEdit.visible" :index="4"> - <form class="default" @submit.prevent=""> - <label> - <translate>Sichtbar ab</translate> - <input type="date" v-model="currentElement.attributes['release-date']" /> - </label> - <label> - <translate>Unsichtbar ab</translate> - <input type="date" v-model="currentElement.attributes['withdraw-date']" /> - </label> - </form> - </courseware-tab> - </courseware-tabs> - </template> - </studip-dialog> <courseware-structural-element-dialog-add v-if="showAddDialog" :structuralElement="structuralElement" :isRoot="isRoot" :canEditParent="canEditParent" /> - <studip-dialog - v-if="showInfoDialog" - :title="textInfo.title" - :closeText="textInfo.close" - closeClass="cancel" - @close="showElementInfoDialog(false)" - > - <template v-slot:dialogContent> - <table class="cw-structural-element-info"> - <tr> - <td><translate>Titel</translate>:</td> - <td>{{ structuralElement.attributes.title }}</td> - </tr> - <tr> - <td><translate>Beschreibung</translate>:</td> - <td>{{ structuralElement.attributes.payload.description }}</td> - </tr> - <tr> - <td><translate>Seite wurde erstellt von</translate>:</td> - <td>{{ ownerName }}</td> - </tr> - <tr> - <td><translate>Seite wurde erstellt am</translate>:</td> - <td><iso-date :date="structuralElement.attributes.mkdate" /></td> - </tr> - <tr> - <td><translate>Zuletzt bearbeitet von</translate>:</td> - <td>{{ editorName }}</td> - </tr> - <tr> - <td><translate>Zuletzt bearbeitet am</translate>:</td> - <td><iso-date :date="structuralElement.attributes.chdate" /></td> - </tr> - </table> - </template> - </studip-dialog> - - <studip-dialog - v-if="showOerDialog" - height="600" - width="600" - :title="textOer.title" - :confirmText="textOer.confirm" - confirmClass="accept" - :closeText="textOer.close" - closeClass="cancel" - @close="showElementOerDialog(false)" - @confirm="publishCurrentElement" - > - <template v-slot:dialogContent> - <form v-show="!oerExportRunning" class="default" @submit.prevent=""> - <fieldset> - <legend><translate>Grunddaten</translate></legend> - <label> - <p><translate>Vorschaubild</translate>:</p> - <img - v-if="currentElement.relationships.image.data" - :src="currentElement.relationships.image.meta['download-url']" - width="400" - /> - </label> - <label> - <p><translate>Beschreibung</translate>:</p> - <p>{{ currentElement.attributes.payload.description }}</p> - </label> - <label> - <translate>Niveau</translate>: - <p> - {{ currentElement.attributes.payload.difficulty_start }} - - {{ currentElement.attributes.payload.difficulty_end }} - </p> - </label> - <label> - <translate>Lizenztyp</translate>: - <p>{{ currentLicenseName }}</p> - </label> - <label> - <translate>Sie können diese Daten unter "Seiteneinstellungen" verändern.</translate> - </label> - </fieldset> - <fieldset> - <legend><translate>Einstellungen</translate></legend> - <label> - <translate>Unterseiten veröffentlichen</translate> - <input type="checkbox" v-model="oerChildren" /> - </label> - </fieldset> - </form> - <courseware-companion-box - v-show="oerExportRunning" - :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')" - mood="pointing" - /> - </template> - </studip-dialog> - <studip-dialog - v-if="showSuggestOerDialog" - height="600" - width="600" - :title="textSuggestOer.title" - :confirmText="textSuggestOer.confirm" - confirmClass="accept" - :closeText="textSuggestOer.close" - closeClass="cancel" - @close="updateShowSuggestOerDialog(false)" - @confirm="sendOerSuggestion" - > - <template v-slot:dialogContent> - <p v-translate>Das folgende Courseware-Material wird %{ ownerName } - zur Veröffentlichung im OER Campus vorgeschlagen:</p> - <table class="cw-structural-element-info"> - <tr> - <td><translate>Titel</translate>:</td> - <td>{{ structuralElement.attributes.title }}</td> - </tr> - <tr> - <td><translate>Beschreibung</translate>:</td> - <td>{{ structuralElement.attributes.payload.description }}</td> - </tr> - </table> - <form class="default" @submit.prevent=""> - <label> - <translate>Ihr Vorschlag wird anonym versendet. Falls gewünscht, können Sie - zusätzlich eine Nachricht verfassen:</translate> - <textarea - v-model="additionalText" - class="cw-structural-element-description" - /> - </label> - </form> - </template> - </studip-dialog> <studip-dialog v-if="showDeleteDialog" :title="textDelete.title" @@ -571,30 +303,7 @@ @confirm="deleteCurrentElement" @close="closeDeleteDialog" ></studip-dialog> - <studip-dialog - v-if="showPublicLinkDialog && inContent" - :title="$gettext('Öffentlichen Link für Seite erzeugen')" - :confirmText="$gettext('Erstellen')" - confirmClass="accept" - :closeText="$gettext('Abbrechen')" - closeClass="cancel" - class="cw-structural-element-dialog" - @close="closePublicLinkDialog" - @confirm="createElementPublicLink" - > - <template v-slot:dialogContent> - <form class="default" @submit.prevent=""> - <label> - <translate>Passwort</translate> - <input type="password" v-model="publicLink.password" /> - </label> - <label> - <translate>Ablaufdatum</translate> - <input v-model="publicLink['expire-date']" type="date" class="size-l" /> - </label> - </form> - </template> - </studip-dialog> + <studip-dialog v-if="showRemoveLockDialog" :title="textRemoveLock.title" @@ -604,14 +313,69 @@ @confirm="executeRemoveLock" @close="showElementRemoveLockDialog(false)" ></studip-dialog> + <courseware-structural-element-dialog-settings + v-if="showEditDialog" + :structuralElement="currentElement" + @close="closeEditDialog" + @store="selectCurrent" + /> + <template v-if="showPermissionsDialog && !isTask && !inContent"> + <studip-dialog + v-if="showPermissionScopeDialog" + :title="$gettext('Rechte und Sichtbarkeit')" + :confirm-text="$gettext('Wechseln')" + confirm-class="accept" + :close-text="$gettext('Abbrechen')" + close-class="cancel" + :question="$gettext('Sie haben bereits die Rechte und Sichtbarkeit für das gesamte Lernmaterial eingestellt. Möchten Sie nun die Rechte für einzelne Seiten anpassen? Die bereits festgelegten Rechte werden beibehalten.')" + height="250" + @close="closeEditDialog" + @confirm="switchPermissionScope" + > - <courseware-structural-element-dialog-import v-if="showImportDialog"/> + </studip-dialog> + <courseware-structural-element-dialog-permissions + v-if="showPermissionSettingsDialog" + :structuralElement="currentElement" + @close="closeEditDialog" + @store="selectCurrent" + /> + </template> + <courseware-structural-element-dialog-import v-if="showImportDialog" /> <courseware-structural-element-dialog-copy v-if="showCopyDialog" /> - <courseware-structural-element-dialog-link v-if="showLinkDialog"/> - <courseware-structural-element-dialog-export-chooser v-if="showExportChooserDialog" :canEdit="canEdit" :canVisit="canVisit" /> - <courseware-structural-element-dialog-export v-if="showExportDialog" :structuralElement="currentElement" /> - <courseware-structural-element-dialog-export-pdf v-if="showPdfExportDialog" :structuralElement="currentElement" /> + <courseware-structural-element-dialog-link v-if="showLinkDialog" /> + <courseware-structural-element-dialog-export-chooser + v-if="showExportChooserDialog" + :canEdit="canEdit" + :canVisit="canVisit" + /> + <courseware-structural-element-dialog-export + v-if="showExportDialog" + :structuralElement="currentElement" + /> + <courseware-structural-element-dialog-export-pdf + v-if="showPdfExportDialog" + :structuralElement="currentElement" + /> + <courseware-structural-element-dialog-export-oer + v-if="showOerExportDialog" + :structuralElement="currentElement" + /> + <courseware-structural-element-dialog-oer-suggest + v-if="showSuggestOerDialog" + :structuralElement="structuralElement" + :ownerName="ownerName" + /> <courseware-structural-element-dialog-add-chooser v-if="showAddChooserDialog" /> + <courseware-structural-element-dialog-info + v-if="showInfoDialog" + :structuralElement="currentElement" + :ownerName="ownerName" + /> + <courseware-structural-element-dialog-public-link + v-if="showPublicLinkDialog && inContent" + :structuralElement="structuralElement" + /> <feedback-dialog v-if="showFeedbackDialog" :feedbackElementId="parseInt(feedbackElementId)" @@ -637,10 +401,10 @@ <div v-else> <courseware-companion-box v-if="currentElement !== ''" - :msgCompanion="textCompanionWrongContext" + :msgCompanion="$gettext('Die angeforderte Seite ist nicht Teil dieser Courseware.')" mood="sad" > - <template v-slot:companionActions > + <template v-slot:companionActions> <a class="button" :href="unitRootUrl">{{ $gettext('Lernmaterial neu laden') }}</a> <a class="button" :href="shelfURL">{{ $gettext('Zurück zur Lernmaterialübersicht') }}</a> </template> @@ -666,22 +430,25 @@ import CoursewareStructuralElementDialogImport from './CoursewareStructuralEleme import CoursewareStructuralElementDialogLink from './CoursewareStructuralElementDialogLink.vue'; import CoursewareStructuralElementDialogExportChooser from './CoursewareStructuralElementDialogExportChooser.vue'; import CoursewareStructuralElementDialogExport from './CoursewareStructuralElementDialogExport.vue'; +import CoursewareStructuralElementDialogExportOer from './CoursewareStructuralElementDialogExportOer.vue'; import CoursewareStructuralElementDialogExportPdf from './CoursewareStructuralElementDialogExportPdf.vue'; +import CoursewareStructuralElementDialogOerSuggest from './CoursewareStructuralElementDialogOerSuggest.vue'; +import CoursewareStructuralElementDialogSettings from './CoursewareStructuralElementDialogSettings.vue'; +import CoursewareStructuralElementDialogPermissions from './CoursewareStructuralElementDialogPermissions.vue'; +import CoursewareStructuralElementDialogInfo from './CoursewareStructuralElementDialogInfo.vue'; +import CoursewareStructuralElementDialogPublicLink from './CoursewareStructuralElementDialogPublicLink.vue'; import CoursewareStructuralElementDiscussion from './CoursewareStructuralElementDiscussion.vue'; -import CoursewareStructuralElementPermissions from './CoursewareStructuralElementPermissions.vue'; -import CoursewareContentPermissions from '../CoursewareContentPermissions.vue'; + import CoursewareWelcomeScreen from './CoursewareWelcomeScreen.vue'; import CoursewareExport from '@/vue/mixins/courseware/export.js'; -import CoursewareOerMessage from '@/vue/mixins/courseware/oermessage.js'; + import colorMixin from '@/vue/mixins/courseware/colors.js'; import wizardMixin from '@/vue/mixins/courseware/wizard.js'; import CoursewareCallToActionBox from '../layouts/CoursewareCallToActionBox.vue'; import CoursewareDateInput from '../layouts/CoursewareDateInput.vue'; -import StockImageSelector from '../../stock-images/SelectorDialog.vue'; import StudipDialog from '../../StudipDialog.vue'; import { FocusTrap } from 'focus-trap-vue'; -import IsoDate from '../layouts/IsoDate.vue'; -import FeedbackDialog from '../../feedback/FeedbackDialog.vue' +import FeedbackDialog from '../../feedback/FeedbackDialog.vue'; import FeedbackCreateDialog from '../../feedback/FeedbackCreateDialog.vue'; import StudipFiveStars from '../../feedback/StudipFiveStars.vue'; import StudipProgressIndicator from '../../StudipProgressIndicator.vue'; @@ -702,10 +469,14 @@ export default { CoursewareStructuralElementDialogLink, CoursewareStructuralElementDialogExport, CoursewareStructuralElementDialogExportChooser, + CoursewareStructuralElementDialogExportOer, CoursewareStructuralElementDialogExportPdf, + CoursewareStructuralElementDialogOerSuggest, + CoursewareStructuralElementDialogSettings, + CoursewareStructuralElementDialogPermissions, + CoursewareStructuralElementDialogInfo, + CoursewareStructuralElementDialogPublicLink, CoursewareStructuralElementDiscussion, - CoursewareStructuralElementPermissions, - CoursewareContentPermissions, CoursewareWelcomeScreen, CoursewareCallToActionBox, CoursewareDateInput, @@ -714,51 +485,21 @@ export default { FeedbackCreateDialog, StudipFiveStars, FocusTrap, - IsoDate, - StockImageSelector, StudipDialog, StudipProgressIndicator, draggable, }), props: ['canVisit', 'orderedStructuralElements', 'structuralElement'], - mixins: [CoursewareExport, CoursewareOerMessage, colorMixin, wizardMixin, containerMixin], + mixins: [CoursewareExport, colorMixin, wizardMixin, containerMixin], data() { return { currentElement: '', - uploadFileError: '', - textCompanionWrongContext: this.$gettext('Die angeforderte Seite ist nicht Teil dieser Courseware.'), - textEdit: { - title: this.$gettext('Seiteneinstellungen'), - confirm: this.$gettext('Speichern'), - close: this.$gettext('Schließen'), - basic: this.$gettext('Grunddaten'), - image: this.$gettext('Bild'), - meta: this.$gettext('Metadaten'), - approval: this.$gettext('Rechte'), - visible: this.$gettext('Sichtbarkeit'), - }, - textInfo: { - title: this.$gettext('Informationen zur Seite'), - close: this.$gettext('Schließen'), - }, - textAdd: { - title: this.$gettext('Seite hinzufügen'), - confirm: this.$gettext('Erstellen'), - close: this.$gettext('Schließen'), - }, - textRibbon: { - perv: this.$gettext('zurück'), - next: this.$gettext('weiter'), - }, textRemoveLock: { title: this.$gettext('Sperre aufheben'), alert: this.$gettext('Möchten Sie die Sperre der Seite wirklich aufheben?'), }, - oerExportRunning: false, - oerChildren: true, - pdfExportChildren: false, containerList: [], isDragging: false, dragOptions: { @@ -769,26 +510,18 @@ export default { }, errorEmptyChapterName: false, consumModeTrap: false, - additionalText: '', - - publicLink: { - passsword: '', - 'expire-date': '' - }, - deletingPreviewImage: false, keyboardSelected: null, assistiveLive: '', - uploadImageURL: null, - showStockImageSelector: false, - selectedStockImage: null, displayFeedback: false, - showRatingPopup: false, ratingPopupFeedbackElement: null, storing: false, handleDebouncedScroll: null, scrollHasBeenPerformed: false, + + showPermissionScopeDialog: false, + showPermissionSettingsDialog: false, }; }, @@ -796,6 +529,7 @@ export default { ...mapGetters({ courseware: 'courseware', rootId: 'rootId', + currentUnit: 'currentUnit', context: 'context', consumeMode: 'consumeMode', containerById: 'courseware-containers/byId', @@ -819,17 +553,17 @@ export default { showPdfExportDialog: 'showStructuralElementPdfExportDialog', showInfoDialog: 'showStructuralElementInfoDialog', showDeleteDialog: 'showStructuralElementDeleteDialog', - showOerDialog: 'showStructuralElementOerDialog', + showOerExportDialog: 'showStructuralElementOerDialog', showSuggestOerDialog: 'showSuggestOerDialog', showPublicLinkDialog: 'showStructuralElementPublicLinkDialog', showRemoveLockDialog: 'showStructuralElementRemoveLockDialog', showFeedbackDialog: 'showStructuralElementFeedbackDialog', showFeedbackCreateDialog: 'showStructuralElementFeedbackCreateDialog', + showPermissionsDialog: 'showStructuralElementPermissionsDialog', oerCampusEnabled: 'oerCampusEnabled', oerEnableSuggestions: 'oerEnableSuggestions', licenses: 'licenses', userId: 'userId', - viewMode: 'viewMode', taskById: 'courseware-tasks/byId', userById: 'users/byId', lastCreatedElement: 'courseware-structural-elements/lastCreated', @@ -869,22 +603,6 @@ export default { return 0; }, - textOer() { - return { - title: this.$gettext('Seite auf dem OER Campus veröffentlichen'), - confirm: this.$gettext('Veröffentlichen'), - close: this.$gettext('Abbrechen'), - }; - }, - - textSuggestOer() { - return { - title: this.$gettext('Seite für den OER Campus vorschlagen'), - confirm: this.$gettext('Vorschlagen'), - close: this.$gettext('Abbrechen'), - }; - }, - inCourse() { return this.context.type === 'courses'; }, @@ -899,11 +617,10 @@ export default { textDelete.title = this.$gettext('Seite unwiderruflich löschen'); textDelete.alert = this.$gettext('Möchten Sie die Seite wirklich löschen?'); if (this.structuralElementLoaded) { - textDelete.alert = - this.$gettextInterpolate( - this.$gettext('Möchten Sie die Seite %{ pageTitle } und alle ihre Unterseiten wirklich löschen?'), - {pageTitle: this.structuralElement.attributes.title} - ); + textDelete.alert = this.$gettextInterpolate( + this.$gettext('Möchten Sie die Seite %{ pageTitle } und alle ihre Unterseiten wirklich löschen?'), + { pageTitle: this.structuralElement.attributes.title } + ); } return textDelete; @@ -942,30 +659,11 @@ export default { } } - return false; }, - image() { - if (this.selectedStockImage) { - return this.selectedStockImage.attributes['download-urls'].small - } - if (this.uploadImageURL) { - return this.uploadImageURL; - } - return this.structuralElement.relationships?.image?.meta?.['download-url'] ?? null; - }, - - imageType() { - return this.structuralElement.relationships?.image?.data?.type ?? null; - }, - - hasImage() { - return (this.image || this.selectedStockImage ) && this.deletingPreviewImage === false; - }, - structuralElementLoaded() { - return this.structuralElement !== null && this.structuralElement !== {}; + return this.structuralElement !== null; }, ancestors() { @@ -1099,19 +797,6 @@ export default { return true; }, - editor() { - const editor = this.relatedUsers({ - parent: this.structuralElement, - relationship: 'editor', - }); - - return editor ?? null; - }, - - editorName() { - return this.editor?.attributes['formatted-name'] ?? '?'; - }, - feedbackElementId() { return this.currentElement?.relationships?.['feedback-element']?.data?.id; }, @@ -1134,8 +819,6 @@ export default { menuItems() { let menu = []; - - if (this.canEdit) { menu.push({ id: 1, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' }); menu.push({ id: 2, label: this.$gettext('Seite exportieren'), icon: 'export', emit: 'exportElement' }); @@ -1157,30 +840,38 @@ export default { emit: 'editCurrentElement', }); if (this.userIsTeacher) { - menu.push({ id: 7, type: 'separator'}); + if (!this.isTask && !this.inContent && !this.isRoot) { + menu.push({ + id: 6, + label: this.$gettext('Rechte & Sichtbarkeit'), + icon: 'lock-unlocked', + emit: 'showPermissions' + }); + } + menu.push({ id: 8, type: 'separator'}); menu.push({ - id: 8, + id: 9, label: this.commentable - ? this.$gettext('Kommentare abschalten') - : this.$gettext('Kommentare aktivieren'), - icon: 'comment2', - emit: this.commentable ? 'deactivateComments' : 'activateComments', + ? this.$gettext('Kommentare abschalten') + : this.$gettext('Kommentare aktivieren'), + icon: 'comment2', + emit: this.commentable ? 'deactivateComments' : 'activateComments', }); if (!this.hasFeedback && !this.displayFeedback) { menu.push({ - id: 9, + id: 10, label: this.$gettext('Anmerkungen aktivieren'), icon: 'exclaim-circle', emit: 'showNote' }); } } - menu.push({ id: 11, type: 'separator'}); + menu.push({ id: 12, type: 'separator'}); } if (this.deletable && this.canEdit && !this.isTask && !this.blocked) { menu.push({ - id: 6, + id: 7, label: this.$gettext('Seite löschen'), icon: 'trash', emit: 'deleteCurrentElement', @@ -1190,7 +881,7 @@ export default { if (this.isFeedbackActivated) { if (this.canCreateFeedbackElement && !this.hasFeedbackElement) { menu.push({ - id: 10, + id: 11, label: this.$gettext('Feedback aktivieren'), icon: 'feedback', emit: 'showFeedbackCreate', @@ -1198,30 +889,35 @@ export default { } if (this.hasFeedbackElement) { menu.push({ - id: 10, + id: 11, label: this.$gettext('Feedback anzeigen'), icon: 'feedback', emit: 'showFeedback', }); } } - menu.push({ id: 12, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' }); + menu.push({ id: 13, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' }); if (this.oerEnableSuggestions && this.inCourse && this.userId !== this.structuralElement.relationships.owner.data.id) { menu.push( - { id: 13, label: this.$gettext('Seite für OER Campus vorschlagen'), icon: 'oer-campus', + { id: 14, label: this.$gettext('Seite für OER Campus vorschlagen'), icon: 'oer-campus', emit: 'showSuggest' } ); } if (this.context.type === 'users') { - menu.push({ id: 14, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' }); + menu.push({ + id: 15, + label: this.$gettext('Öffentlichen Link erzeugen'), + icon: 'group', + emit: 'linkElement', + }); } if (!document.documentElement.classList.contains('responsive-display')) { - menu.push({ id: 15, type: 'separator'}); + menu.push({ id: 16, type: 'separator'}); menu.push( - { id: 16, label: this.$gettext('Als Vollbild anzeigen'), icon: 'screen-full', + { id: 17, label: this.$gettext('Als Vollbild anzeigen'), icon: 'screen-full', emit: 'activateFullscreen'}, ); } @@ -1231,20 +927,12 @@ export default { return menu; }, colors() { - return this.mixinColors.filter(color => color.darkmode); + return this.mixinColors.filter((color) => color.darkmode); }, - currentLicenseName() { - for (let i = 0; i < this.licenses.length; i++) { - if (this.licenses[i]['id'] == this.currentElement.attributes.payload.license_type) { - return this.licenses[i]['name']; - } - } - return ''; - }, blockingUser() { if (this.blockedByAnotherUser) { - return this.userById({id: this.blockerId}); + return this.userById({ id: this.blockerId }); } return null; @@ -1318,7 +1006,7 @@ export default { linkedElement() { if (this.isLink) { - return this.structuralElementById({ id: this.structuralElement.attributes['target-id']}); + return this.structuralElementById({ id: this.structuralElement.attributes['target-id'] }); } return null; @@ -1330,13 +1018,12 @@ export default { if (relatedContainers) { for (const container of relatedContainers) { - containers.push(this.containerById({ id: container.id})); + containers.push(this.containerById({ id: container.id })); } } return containers; }, - owner() { const owner = this.relatedUsers({ parent: this.structuralElement, @@ -1362,16 +1049,12 @@ export default { return ''; }, shelfURL() { - return STUDIP.URLHelper.getURL( - 'dispatch.php/course/courseware/', - {cid: this.context.id} - ); + return STUDIP.URLHelper.getURL('dispatch.php/course/courseware/', { cid: this.context.id }); }, unitRootUrl() { - return STUDIP.URLHelper.getURL( - 'dispatch.php/course/courseware/courseware/' + this.context.unit, - {cid: this.context.id} - ); + return STUDIP.URLHelper.getURL('dispatch.php/course/courseware/courseware/' + this.context.unit, { + cid: this.context.id, + }); }, commentable() { return this.currentElement?.attributes?.commentable ?? false; @@ -1401,7 +1084,8 @@ export default { '%{length} Anmerkungen zur Seite (Nur für Nutzende mit Schreibrechten sichtbar)', this.feedbackCounter ), - { length: this.feedbackCounter }); + { length: this.feedbackCounter } + ); }, comments() { const parent = { @@ -1416,18 +1100,14 @@ export default { }, callToActionTitleComments() { return this.$gettextInterpolate( - this.$ngettext( - '%{length} Kommentar zur Seite', - '%{length} Kommentare zur Seite', - this.commentsCounter - ), - { length: this.commentsCounter }); + this.$ngettext('%{length} Kommentar zur Seite', '%{length} Kommentare zur Seite', this.commentsCounter), + { length: this.commentsCounter } + ); }, }, methods: { ...mapActions({ - updateStructuralElement: 'updateStructuralElement', deleteStructuralElement: 'deleteStructuralElement', lockObject: 'lockObject', unlockObject: 'unlockObject', @@ -1436,9 +1116,6 @@ export default { companionWarning: 'companionWarning', companionError: 'companionError', companionSuccess: 'companionSuccess', - uploadImageForStructuralElement: 'uploadImageForStructuralElement', - deleteImageForStructuralElement: 'deleteImageForStructuralElement', - setStockImageForStructuralElement: 'setStockImageForStructuralElement', showElementEditDialog: 'showElementEditDialog', showElementAddDialog: 'showElementAddDialog', showElementAddChooserDialog: 'showElementAddChooserDialog', @@ -1447,18 +1124,17 @@ export default { showElementPdfExportDialog: 'showElementPdfExportDialog', showElementInfoDialog: 'showElementInfoDialog', showElementDeleteDialog: 'showElementDeleteDialog', - showElementOerDialog: 'showElementOerDialog', showElementPublicLinkDialog: 'showElementPublicLinkDialog', showElementRemoveLockDialog: 'showElementRemoveLockDialog', updateShowSuggestOerDialog: 'updateShowSuggestOerDialog', showStructuralElementFeedbackDialog: 'showStructuralElementFeedbackDialog', showStructuralElementFeedbackCreateDialog: 'showStructuralElementFeedbackCreateDialog', + showStructuralElementPermissionsDialog: 'showStructuralElementPermissionsDialog', updateContainer: 'updateContainer', createContainer: 'createContainer', sortContainersInStructualElements: 'sortContainersInStructualElements', loadTask: 'loadTask', loadStructuralElement: 'loadStructuralElement', - createLink: 'createLink', setCurrentElementId: 'coursewareCurrentElement', loadProgresses: 'loadProgresses', activateStructuralElementComments: 'activateStructuralElementComments', @@ -1467,24 +1143,17 @@ export default { createFeedback: 'feedback-elements/create', loadFeedbackElement: 'feedback-elements/loadById', setProcessing: 'setProcessing', + updateUnit: 'courseware-units/update', + loadUnit: 'courseware-units/loadById', }), initCurrent() { - if (!this.storing) { - this.currentElement = _.cloneDeep(this.structuralElement); - this.uploadFileError = ''; - this.deletingPreviewImage = false; - this.uploadImageURL = null; - this.loadFeedback(); - } + this.currentElement = _.cloneDeep(this.structuralElement); + this.loadFeedback(); }, async menuAction(action) { - switch (action) { - case 'removeLock': - this.displayRemoveLockDialog(); - break; - case 'editCurrentElement': - await this.loadStructuralElement(this.currentId); + if (['editCurrentElement', 'showPermissions'].includes(action)) { + await this.loadStructuralElement(this.currentId); if (this.blockedByAnotherUser) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); @@ -1492,7 +1161,7 @@ export default { } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); - } catch(error) { + } catch (error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); } else { @@ -1501,9 +1170,17 @@ export default { return false; } - this.initCurrent(); + } + switch (action) { + case 'removeLock': + this.displayRemoveLockDialog(); + break; + case 'editCurrentElement': this.showElementEditDialog(true); break; + case 'showPermissions': + this.showStructuralElementPermissionsDialog(true); + break; case 'addElement': this.errorEmptyChapterName = false; this.showElementAddChooserDialog(true); @@ -1517,8 +1194,8 @@ export default { this.companionInfo({ info: this.$gettextInterpolate( this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Seite bearbeitet.'), - {blockingUserName: this.blockingUserName} - ) + { blockingUserName: this.blockingUserName } + ), }); return false; @@ -1555,99 +1232,38 @@ export default { break; case 'showNote': this.displayFeedback = true; + break; } }, + selectCurrent() { + this.$emit('select', this.currentId); + }, async closeEditDialog() { - await this.loadStructuralElement(this.currentElement.id); if (this.blockedByThisUser) { await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); await this.loadStructuralElement(this.currentElement.id); } + this.showPermissionScopeDialog = false; + this.showPermissionSettingsDialog = false; this.showElementEditDialog(false); - this.initCurrent(); + this.showStructuralElementPermissionsDialog(false); + }, + async switchPermissionScope() { + const unit = { + id: this.currentUnit.id, + type: 'courseware-units', + attributes: { + 'permission-scope': 'structural_element', + }, + }; + await this.updateUnit(unit); + await this.loadUnit({ id: this.currentUnit.id }); + this.showPermissionScopeDialog = false; + this.showPermissionSettingsDialog = true; }, closeAddDialog() { this.showElementAddDialog(false); }, - checkUploadFile() { - const file = this.$refs?.upload_image?.files[0]; - this.uploadImageURL = null; - this.uploadFileError = this.checkUploadImageFile(this.$refs?.upload_image?.files[0]); - if (this.uploadFileError === '') { - this.deletingPreviewImage = false; - this.uploadImageURL = window.URL.createObjectURL(file); - } - }, - deleteImage() { - if (!this.deletingPreviewImage) { - this.deletingPreviewImage = true; - } - }, - async storeCurrentElement() { - this.storing = true; - await this.loadStructuralElement(this.currentElement.id); - if (this.blockedByAnotherUser) { - this.companionWarning({ - info: this.$gettextInterpolate( - this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'), - {blockingUserName: this.blockingUserName} - ) - }); - this.showElementEditDialog(false); - this.storing = false; - return false; - } - if (!this.blocked) { - await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); - } - - const file = this.$refs?.upload_image?.files[0]; - try { - this.uploadFileError = ''; - if (file) { - await this.uploadImageForStructuralElement({ - structuralElement: this.currentElement, - file, - }); - } else if (this.selectedStockImage) { - await this.setStockImageForStructuralElement({ - structuralElement: this.currentElement, - stockImage: this.selectedStockImage, - }) - } else if (this.deletingPreviewImage) { - await this.deleteImageForStructuralElement(this.currentElement); - } - - this.loadStructuralElement(this.currentElement.id); - } catch(error) { - console.error(error); - this.uploadFileError = this.$gettext('Das Bild für das neue Lernmaterial konnte nicht gespeichert werden.'); - } - - this.showElementEditDialog(false); - if (this.currentElement.attributes['release-date'] !== '') { - this.currentElement.attributes['release-date'] = - new Date(this.currentElement.attributes['release-date']).getTime() / 1000; - } - - if (this.currentElement.attributes['withdraw-date'] !== '') { - this.currentElement.attributes['withdraw-date'] = - new Date(this.currentElement.attributes['withdraw-date']).getTime() / 1000; - } - - const element = { - id: this.currentElement.id, - type: this.currentElement.type, - attributes: this.currentElement.attributes, - }; - - await this.updateStructuralElement({ element, id: this.currentId}); - await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); - this.$emit('select', this.currentId); - this.storing = false; - this.initCurrent(); - }, - dropContainer() { this.isDragging = false; this.storeSort(); @@ -1679,24 +1295,12 @@ export default { structuralElement: this.structuralElement, containers: this.containerList, }); - this.$emit('select', this.currentId); + this.selectCurrent(); clearTimeout(timeout); this.setProcessing(false); }, - - - async publishCurrentElement() { - if (this.oerExportRunning) { - return; - } - this.oerExportRunning = true; - await this.exportToOER(this.currentElement, { withChildren: this.oerChildren }); - this.oerExportRunning = false; - this.showElementOerDialog(false); - }, - async closeDeleteDialog() { await this.loadStructuralElement(this.currentElement.id); if (this.blockedByThisUser) { @@ -1708,7 +1312,7 @@ export default { await this.loadStructuralElement(this.currentElement.id); if (!this.deletable) { this.companionWarning({ - info: this.$gettext('Diese Seite darf nicht gelöscht werden') + info: this.$gettext('Diese Seite darf nicht gelöscht werden'), }); this.showElementDeleteDialog(false); return false; @@ -1717,8 +1321,8 @@ export default { this.companionWarning({ info: this.$gettextInterpolate( this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'), - {blockingUserName: this.blockingUserName} - ) + { blockingUserName: this.blockingUserName } + ), }); this.showElementDeleteDialog(false); return false; @@ -1730,13 +1334,13 @@ export default { id: this.currentId, parentId: this.structuralElement.relationships.parent.data.id, }) - .then(response => { - this.$router.push(redirect_id); - this.companionInfo({ info: this.$gettext('Die Seite wurde gelöscht.') }); - }) - .catch(error => { - this.companionError({ info: this.$gettext('Die Seite konnte nicht gelöscht werden.') }); - }); + .then((response) => { + this.$router.push(redirect_id); + this.companionInfo({ info: this.$gettext('Die Seite wurde gelöscht.') }); + }) + .catch((error) => { + this.companionError({ info: this.$gettext('Die Seite konnte nicht gelöscht werden.') }); + }); }, containerComponent(container) { return 'courseware-' + container.attributes['container-type'] + '-container'; @@ -1745,46 +1349,6 @@ export default { this.addBookmark(this.structuralElement); this.companionInfo({ info: this.$gettext('Das Lesezeichen wurde gesetzt.') }); }, - updateReadApproval(approval) { - this.currentElement.attributes['read-approval'] = approval; - }, - updateWriteApproval(approval) { - this.currentElement.attributes['write-approval'] = approval; - }, - sendOerSuggestion() { - this.suggestViaAction(this.currentElement, this.additionalText); - this.updateShowSuggestOerDialog(false); - }, - async createElementPublicLink() { - const date = this.publicLink['expire-date']; - const publicLink = { - attributes: { - password: this.publicLink.password, - 'expire-date': date === '' ? new Date(0).toISOString() : new Date(date).toISOString() - }, - relationships: { - 'structural-element': { - data: { - id: this.currentElement.id, - type: 'courseware-structural-elements' - } - } - } - } - - await this.createLink({ publicLink }); - this.companionSuccess({ - info: this.$gettext('Öffentlicher Link wurde angelegt. Unter Freigaben finden Sie alle Ihre öffentlichen Links.'), - }); - this.closePublicLinkDialog(); - }, - closePublicLinkDialog() { - this.publicLink = { - passsword: '', - 'expire-date': '' - }; - this.showElementPublicLinkDialog(false); - }, displayRemoveLockDialog() { this.showElementRemoveLockDialog(true); }, @@ -1824,13 +1388,18 @@ export default { this.storeKeyboardSorting(containerId); } else { this.keyboardSelected = containerId; - const container = this.containerById({id: containerId}); - const index = this.containerList.findIndex(c => c.id === container.id); - this.assistiveLive = - this.$gettextInterpolate( - this.$gettext('%{containerTitle} Abschnitt ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.') - , {containerTitle: container.attributes.title, pos: index + 1, listLength: this.containerList.length} - ); + const container = this.containerById({ id: containerId }); + const index = this.containerList.findIndex((c) => c.id === container.id); + this.assistiveLive = this.$gettextInterpolate( + this.$gettext( + '%{containerTitle} Abschnitt ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.' + ), + { + containerTitle: container.attributes.title, + pos: index + 1, + listLength: this.containerList.length, + } + ); } break; } @@ -1851,60 +1420,67 @@ export default { } }, moveItemUp(containerId) { - const currentIndex = this.containerList.findIndex(container => container.id === containerId); + const currentIndex = this.containerList.findIndex((container) => container.id === containerId); if (currentIndex !== 0) { - const container = this.containerById({id: containerId}); + const container = this.containerById({ id: containerId }); const newPos = currentIndex - 1; this.containerList.splice(newPos, 0, this.containerList.splice(currentIndex, 1)[0]); - this.assistiveLive = - this.$gettextInterpolate( - this.$gettext('%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.') - , {containerTitle: container.attributes.title, pos: newPos + 1, listLength: this.containerList.length} - ); + this.assistiveLive = this.$gettextInterpolate( + this.$gettext( + '%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.' + ), + { + containerTitle: container.attributes.title, + pos: newPos + 1, + listLength: this.containerList.length, + } + ); } }, moveItemDown(containerId) { - const currentIndex = this.containerList.findIndex(container => container.id === containerId); + const currentIndex = this.containerList.findIndex((container) => container.id === containerId); if (this.containerList.length - 1 > currentIndex) { - const container = this.containerById({id: containerId}); + const container = this.containerById({ id: containerId }); const newPos = currentIndex + 1; this.containerList.splice(newPos, 0, this.containerList.splice(currentIndex, 1)[0]); - this.assistiveLive = - this.$gettextInterpolate( - this.$gettext('%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.') - , {containerTitle: container.attributes.title, pos: newPos + 1, listLength: this.containerList.length} - ); + this.assistiveLive = this.$gettextInterpolate( + this.$gettext( + '%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.' + ), + { + containerTitle: container.attributes.title, + pos: newPos + 1, + listLength: this.containerList.length, + } + ); } }, abortKeyboardSorting(containerId) { - const container = this.containerById({id: containerId}); + const container = this.containerById({ id: containerId }); this.keyboardSelected = null; - this.assistiveLive = - this.$gettextInterpolate( - this.$gettext('%{containerTitle} Abschnitt, Neuordnung abgebrochen.') - , {containerTitle: container.attributes.title} - ); - this.$emit('select', this.currentId); + this.assistiveLive = this.$gettextInterpolate( + this.$gettext('%{containerTitle} Abschnitt, Neuordnung abgebrochen.'), + { containerTitle: container.attributes.title } + ); + this.selectCurrent(); }, storeKeyboardSorting(containerId) { - const container = this.containerById({id: containerId}); - const currentIndex = this.containerList.findIndex(container => container.id === containerId); + const container = this.containerById({ id: containerId }); + const currentIndex = this.containerList.findIndex((container) => container.id === containerId); this.keyboardSelected = null; - this.assistiveLive = - this.$gettextInterpolate( - this.$gettext('%{containerTitle} Abschnitt, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.') - , {containerTitle: container.attributes.title, pos: currentIndex + 1, listLength: this.containerList.length} - ); + this.assistiveLive = this.$gettextInterpolate( + this.$gettext( + '%{containerTitle} Abschnitt, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.' + ), + { + containerTitle: container.attributes.title, + pos: currentIndex + 1, + listLength: this.containerList.length, + } + ); this.storeSort(); }, - onSelectStockImage(stockImage) { - if (this.$refs?.upload_image) { - this.$refs.upload_image.value = null; - } - this.selectedStockImage = stockImage; - this.showStockImageSelector = false; - this.deletingPreviewImage = false; - }, + activateFeedback() { const data = { attributes: { @@ -1932,12 +1508,12 @@ export default { let showRatingPopup = false; let ratingPopupFeedbackElement = null; const toId = to.params.id; - const toElem = this.structuralElementById({id: toId}); + const toElem = this.structuralElementById({ id: toId }); if (toId === this.nextElement?.id && toElem.relationships.parent.data.id === this.rootId) { const firstLevelElement = await this.findFirstLevelParent(this.currentElement); const feedbackElementId = firstLevelElement?.relationships?.['feedback-element']?.data?.id; if (feedbackElementId) { - await this.loadFeedbackElement({ id: feedbackElementId, options: { include: 'entries' }}); + await this.loadFeedbackElement({ id: feedbackElementId, options: { include: 'entries' } }); ratingPopupFeedbackElement = this.getFeedbackElementById({ id: feedbackElementId }); const hasUserEntry = this.feedbackEntries.filter( (entry) => @@ -2047,7 +1623,7 @@ export default { this.showFeedbackPopup(to, from); } }, - deep: true + deep: true, }, structuralElement: { async handler() { @@ -2071,7 +1647,7 @@ export default { this.loadFeedbackElement({ id: this.feedbackElementId }); } }, - deep: true + deep: true, }, containers() { this.containerList = this.containers; @@ -2082,13 +1658,24 @@ export default { this.$nextTick(() => { const selected = this.$refs['sortableHandle' + this.keyboardSelected][0]; selected.focus(); - selected.scrollIntoView({behavior: "smooth", block: "center"}); + selected.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); } }, consumeMode(newState) { this.consumModeTrap = newState; }, + showPermissionsDialog(newVal) { + if (newVal) { + if (this.currentUnit.attributes['permission-scope'] !== 'structural_element') { + this.showPermissionScopeDialog = true; + this.showPermissionSettingsDialog = false; + } else { + this.showPermissionScopeDialog = false; + this.showPermissionSettingsDialog = true; + } + } + } }, // this line provides all the components to courseware plugins diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportChooser.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportChooser.vue index be5dd8d52ddbc62b80889f9e3022f45e24921a63..ec072900aa56a0c333c700b44b29d144aba13e3d 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportChooser.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportChooser.vue @@ -89,7 +89,7 @@ export default { showElementExportDialog: 'showElementExportDialog', showElementExportChooserDialog: 'showElementExportChooserDialog', showElementPdfExportDialog: 'showElementPdfExportDialog', - showElementOerDialog: 'showElementOerDialog', + showElementOerExportDialog: 'showElementOerExportDialog', }), selectType(type) { switch (type) { @@ -100,7 +100,7 @@ export default { this.showElementPdfExportDialog(true); break; case 'oer': - this.showElementOerDialog(true); + this.showElementOerExportDialog(true); break; } this.showElementExportChooserDialog(false); diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportOer.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportOer.vue new file mode 100644 index 0000000000000000000000000000000000000000..8059ebfb4cde7e692c5a03b6d6743ddd31947be4 --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportOer.vue @@ -0,0 +1,110 @@ +<template> + <studip-dialog + height="600" + width="600" + :title="$gettext('Seite auf OER Campus veröffentlichen')" + :confirmText="$gettext('Veröffentlichen')" + confirmClass="accept" + :closeText="$gettext('Abbrechen')" + closeClass="cancel" + @close="showElementOerExportDialog(false)" + @confirm="publishStructuralElement" + > + <template v-slot:dialogContent> + <form v-show="!oerExportRunning" class="default" @submit.prevent=""> + <fieldset> + <legend>{{ $gettext('Grunddaten') }}</legend> + <label> + <p>{{ $gettext('Vorschaubild') }}:</p> + <img + v-if="structuralElement.relationships.image.data" + :src="structuralElement.relationships.image.meta['download-url']" + width="400" + /> + </label> + <label> + <p>{{ $gettext('Beschreibung') }}:</p> + <p>{{ structuralElement.attributes.payload.description }}</p> + </label> + <label> + {{ $gettext('Niveau') }}: + <p> + {{ structuralElement.attributes.payload.difficulty_start }} - + {{ structuralElement.attributes.payload.difficulty_end }} + </p> + </label> + <label> + {{ $gettext('Lizenztyp') }}: + <p>{{ currentLicenseName }}</p> + </label> + <label> + {{ $gettext('Sie können diese Daten unter „Seiteneinstellungen“ verändern.') }} + </label> + </fieldset> + <fieldset> + <legend>{{ $gettext('Einstellungen') }}</legend> + <label> + {{ $gettext('Unterseiten veröffentlichen') }} + <input type="checkbox" v-model="oerExportChildren" /> + </label> + </fieldset> + </form> + <courseware-companion-box + v-show="oerExportRunning" + :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')" + mood="pointing" + /> + </template> + </studip-dialog> +</template> + +<script> +import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; +import CoursewareExport from '@/vue/mixins/courseware/export.js'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-structural-element-dialog-export-oer', + mixins: [CoursewareExport], + components: { CoursewareCompanionBox }, + props: { + structuralElement: Object, + }, + data() { + return { + oerExportChildren: false, + oerExportRunning: false, + }; + }, + computed: { + ...mapGetters({ + context: 'context', + licenses: 'licenses', + }), + currentLicenseName() { + for (let i = 0; i < this.licenses.length; i++) { + if (this.licenses[i]['id'] == this.structuralElement.attributes.payload.license_type) { + return this.licenses[i]['name']; + } + } + + return ''; + }, + }, + methods: { + ...mapActions({ + showElementOerExportDialog: 'showElementOerExportDialog', + }), + + async publishStructuralElement() { + if (this.oerExportRunning) { + return; + } + this.oerExportRunning = true; + await this.exportToOER(this.currentElement, { withChildren: this.oerChildren }); + this.oerExportRunning = false; + this.showElementOerDialog(false); + }, + } +}; +</script> diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportPdf.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportPdf.vue index a2aca9d42f249bdf0ebf1639858e1a749ad115b3..7cb88d9bf709577839d45a9f0f3043dd7985630e 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportPdf.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportPdf.vue @@ -1,9 +1,9 @@ <template> <studip-dialog :title="$gettext('Seite exportieren')" - :confirmText="this.$gettext('Erstellen')" + :confirmText="$gettext('Erstellen')" confirmClass="accept" - :closeText="this.$gettext('Schließen')" + :closeText="$gettext('Abbrechen')" closeClass="cancel" height="350" @close="showElementPdfExportDialog(false)" @@ -29,7 +29,7 @@ import { mapActions, mapGetters } from 'vuex'; export default { - name: 'courseware-structural-element-dialog-export', + name: 'courseware-structural-element-dialog-export-pdf', props: { structuralElement: Object, }, diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogInfo.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogInfo.vue new file mode 100644 index 0000000000000000000000000000000000000000..fcc5a8796be8c0c0f4acea243d1bc6f40c0a8175 --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogInfo.vue @@ -0,0 +1,72 @@ +<template> + <studip-dialog + :title="$gettext('Informationen zur Seite')" + :closeText="$gettext('Schließen')" + @close="showElementInfoDialog(false)" + > + <template v-slot:dialogContent> + <table class="cw-structural-element-info"> + <tr> + <td>{{ $gettext('Titel') }}:</td> + <td>{{ structuralElement.attributes.title }}</td> + </tr> + <tr> + <td>{{ $gettext('Beschreibung') }}:</td> + <td>{{ structuralElement.attributes.payload.description }}</td> + </tr> + <tr> + <td>{{ $gettext('Seite wurde erstellt von') }}:</td> + <td>{{ ownerName }}</td> + </tr> + <tr> + <td>{{ $gettext('Seite wurde erstellt am') }}:</td> + <td><iso-date :date="structuralElement.attributes.mkdate" /></td> + </tr> + <tr> + <td>{{ $gettext('Zuletzt bearbeitet von') }}:</td> + <td>{{ editorName }}</td> + </tr> + <tr> + <td>{{ $gettext('Zuletzt bearbeitet am') }}:</td> + <td><iso-date :date="structuralElement.attributes.chdate" /></td> + </tr> + </table> + </template> + </studip-dialog> +</template> +<script> +import IsoDate from '../layouts/IsoDate.vue'; +import { mapActions, mapGetters } from 'vuex'; +export default { + name: 'courseware-structural-element-dialog-info', + components: { + IsoDate, + }, + props: { + structuralElement: Object, + ownerName: String, + }, + computed: { + ...mapGetters({ + relatedUsers: 'users/related', + }), + editor() { + const editor = this.relatedUsers({ + parent: this.structuralElement, + relationship: 'editor', + }); + + return editor ?? null; + }, + + editorName() { + return this.editor?.attributes['formatted-name'] ?? '?'; + }, + }, + methods: { + ...mapActions({ + showElementInfoDialog: 'showElementInfoDialog', + }), + }, +}; +</script> diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogOerSuggest.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogOerSuggest.vue new file mode 100644 index 0000000000000000000000000000000000000000..30a57d441289dd3e39e3f022d08330c87f163548 --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogOerSuggest.vue @@ -0,0 +1,72 @@ +<template> + <studip-dialog + height="600" + width="600" + :title="$gettext('Seite für OER Campus vorschlagen')" + :confirmText="$gettext('Vorschlagen')" + confirmClass="accept" + :closeText="$gettext('Abbrechen')" + closeClass="cancel" + @close="updateShowSuggestOerDialog(false)" + @confirm="sendOerSuggestion" + > + <template v-slot:dialogContent> + <p> + {{ + $gettextInterpolate( + $gettext( + 'Der folgende Lerninhalt wird %{ ownerName } zur Veröffentlichung im OER Campus vorgeschlagen:' + ), + { ownerName: ownerName } + ) + }} + </p> + <table class="cw-structural-element-info"> + <tr> + <td>{{ $gettext('Titel') }}:</td> + <td>{{ structuralElement.attributes.title }}</td> + </tr> + <tr> + <td>{{ $gettext('Beschreibung') }}:</td> + <td>{{ structuralElement.attributes.payload.description }}</td> + </tr> + </table> + <form class="default" @submit.prevent=""> + <label> + {{ + $gettext( + 'Ihr Vorschlag wird anonym versendet. Falls gewünscht, können Sie zusätzlich eine Nachricht verfassen' + ) + }} + <textarea v-model="additionalText" class="cw-structural-element-description"></textarea> + </label> + </form> + </template> + </studip-dialog> +</template> +<script> +import CoursewareOerMessage from '@/vue/mixins/courseware/oermessage.js'; +import { mapActions } from 'vuex'; +export default { + name: 'courseware-structural-element-dialog-oer-suggest', + mixins: [CoursewareOerMessage], + props: { + structuralElement: Object, + ownerName: String + }, + data() { + return { + additionalText: '', + } + }, + methods: { + ...mapActions({ + updateShowSuggestOerDialog: 'updateShowSuggestOerDialog', + }), + sendOerSuggestion() { + this.suggestViaAction(this.structuralElement, this.additionalText); + this.updateShowSuggestOerDialog(false); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue new file mode 100644 index 0000000000000000000000000000000000000000..930cd909ae54774b50a58c21705700f029047e3c --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue @@ -0,0 +1,619 @@ +<template> + <studip-dialog + :title="dialogTitle" + :confirm-text="$gettext('Speichern')" + confirm-class="accept" + :close-text="$gettext('Schließen')" + close-class="cancel" + :height="height" + :width="width" + @close="$emit('close')" + @confirm="storePermissions" + > + <template v-slot:dialogContent> + <div class="cw-permissions-form-wrapper"> + <form class="default cw-permissions-form-radioset" @submit.prevent=""> + <div class="cw-radioset-wrapper" role="group" aria-labelledby="permission-type"> + <p class="sr-only" id="permission-type">{{ $gettext('Typ') }}</p> + <div class="cw-radioset"> + <div class="cw-radioset-box" :class="[permissionType === 'all' ? 'selected' : '']"> + <input + type="radio" + id="permission-type-all" + value="all" + v-model="permissionType" + @change="updatePermissionType" + /> + <label for="permission-type-all"> + <div + class="label-icon all" + :class="[permissionType === 'all' ? 'selected' : '']" + ></div> + <div class="label-text"> + <span>{{ $gettext('alle Studierenden') }}</span> + </div> + </label> + </div> + <div class="cw-radioset-box" :class="[permissionType === 'users' ? 'selected' : '']"> + <input + type="radio" + id="permission-type-users" + value="users" + v-model="permissionType" + @change="updatePermissionType" + /> + <label for="permission-type-users"> + <div + class="label-icon users" + :class="[permissionType === 'users' ? 'selected' : '']" + ></div> + <div class="label-text"> + <span>{{ $gettext('ausgewählte Studierende') }}</span> + </div> + </label> + </div> + <div class="cw-radioset-box" :class="[permissionType === 'groups' ? 'selected' : '']"> + <input + type="radio" + id="permission-type-groups" + value="groups" + v-model="permissionType" + @change="updatePermissionType" + /> + <label for="permission-type-groups"> + <div + class="label-icon groups" + :class="[permissionType === 'groups' ? 'selected' : '']" + ></div> + <div class="label-text"> + <span>{{ $gettext('Gruppen') }}</span> + </div> + </label> + </div> + </div> + </div> + </form> + + <form class="default cw-form-selects" @submit.prevent=""> + <div class="cw-form-selects-row"> + <label> + {{ $gettext('Sichtbar') }} + <select v-model="visible" @change="updateVisibile"> + <option value="always">{{ $gettext('Immer') }}</option> + <option value="period">{{ $gettext('Zeitraum') }}</option> + <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option> + </select> + </label> + <template v-if="visible === 'period'"> + <label> + {{ $gettext('von') }} + <datepicker v-model="visibleStartDate" :placeholder="$gettext('unbegrenzt')" /> + </label> + <label> + {{ $gettext('bis') }} + <datepicker v-model="visibleEndDate" :placeholder="$gettext('unbegrenzt')" /> + </label> + </template> + </div> + <div class="cw-form-selects-row"> + <label + >{{ $gettext('Bearbeitbar') }} + <select v-model="writable" @change="updateWritable"> + <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option> + <option value="always">{{ $gettext('Immer') }}</option> + <option value="period">{{ $gettext('Zeitraum') }}</option> + </select> + </label> + <template v-if="writable === 'period'"> + <div> + <label> + {{ $gettext('von') }} + <datepicker v-model="writableStartDate" :placeholder="$gettext('unbegrenzt')" /> + </label> + </div> + <div> + <label> + {{ $gettext('bis') }} + <datepicker v-model="writableEndDate" :placeholder="$gettext('unbegrenzt')" /> + </label> + </div> + </template> + </div> + </form> + </div> + <div v-if="permissionType === 'all'" class="cw-contents-overview-teaser"> + <div class="cw-contents-overview-teaser-content"> + <header>{{ $gettext('Rechte und Sichtbarkeit') }}</header> + <p> + {{ + $gettext( + 'Hier stellen Sie für diese Seite Ihres Lernmaterials ein, welche Teilnehmenden aus Ihrer Veranstaltung sie sehen bzw. bearbeiten können. Falls Sie eine Einstellung für das gesamte Lernmaterial suchen, können Sie diese Einstellung in der „Übersicht“ über alle Lernmaterialien in Ihrer Veranstaltung vornehmen.' + ) + }} + </p> + <p> + {{ + $gettext( + 'Entscheiden Sie sich zunächst ob „alle Studierende“ die gleichen Rechte erhalten sollen, oder ob „einzelne Studierende“ oder zuvor erstellte „Gruppen“ unterschiedliche Rechte benötigen. Die Einstellung „einzelne Studierende“ oder „Gruppen“ bietet sich beispielsweise dann an, wenn Sie eine Coursewareseite von einer Kleingruppe bearbeiten lassen wollen. Anschließend können Sie einstellen, in welchem Zeitraum diese Rechte gelten.' + ) + }} + </p> + </div> + </div> + + <table v-if="permissionType === 'users'" class="default permission-table"> + <caption> + {{ $gettext('Studierende') }} + </caption> + <thead> + <tr> + <th>{{ $gettext('Name') }}</th> + <th> + {{ $gettext('Sichtbar') }} + <input type="checkbox" v-model="visibleAll" @change="updatewritableAll" /> + </th> + <th> + {{ $gettext('Bearbeitbar') }} + <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" /> + </th> + </tr> + </thead> + <tbody> + <tr v-if="autorMembers.length === 0"> + <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td> + </tr> + <tr v-for="autor in autorMembers" :key="autor.id"> + <td>{{ autor.formattedname }}</td> + <td> + <input + v-if="!visibleAll" + type="checkbox" + :value="autor.id" + v-model="visibleApprovalUsers" + @change="updateUserVisible(autor)" + /> + <studip-icon v-else shape="accept" role="info" :size="14" /> + </td> + <td> + <input + v-if="!writableAll" + type="checkbox" + :value="autor.id" + v-model="writableApprovalUsers" + @change="updateUserWritable(autor)" + /> + <studip-icon v-else shape="accept" role="info" :size="14" /> + </td> + </tr> + </tbody> + </table> + <template v-if="permissionType === 'groups'"> + <table v-if="groups.length > 0" class="default"> + <caption> + {{ $gettext('Gruppen') }} + </caption> + <thead> + <tr> + <th>{{ $gettext('Name') }}</th> + <th> + {{ $gettext('Sichtbar') }} + <input type="checkbox" v-model="visibleAll" :disabled="writableAll" /> + </th> + <th> + {{ $gettext('Bearbeitbar') }} + <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" /> + </th> + </tr> + </thead> + <tbody> + <tr v-if="groups.length === 0"> + <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td> + </tr> + <tr v-for="group in groups" :key="group.id"> + <td>{{ group.name }}</td> + <td> + <input + v-if="!visibleAll" + type="checkbox" + :value="group.id" + v-model="visibleApprovalGroups" + @change="updateGroupVisible(group)" + /> + <studip-icon v-else shape="accept" role="info" :size="14" /> + </td> + <td> + <input + v-if="!writableAll" + type="checkbox" + :value="group.id" + v-model="writableApprovalGroups" + @change="updateGroupWritable(group)" + /> + <studip-icon v-else shape="accept" role="info" :size="14" /> + </td> + </tr> + </tbody> + </table> + <courseware-companion-box + v-else + :msgCompanion=" + $gettext( + 'Sie haben noch keine Gruppen erstellt. Mit Gruppen können Sie die Sichtbarkeits- und Bearbeitungsrechte anschließend besonders unkompliziert an Arbeitsgruppen vergeben.' + ) + " + mood="pointing" + > + <template #companionActions> + <a :href="statusGroupsUrl" + ><button class="button">{{ $gettext('Zu den Gruppen der Veranstaltung') }}</button></a + > + </template> + </courseware-companion-box> + </template> + </template> + </studip-dialog> +</template> + +<script> +import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; +import Datepicker from './../../Datepicker.vue'; +import axios from 'axios'; +import { mapActions, mapGetters } from 'vuex'; +export default { + name: 'courseware-structural-element-dialog-permissions', + components: { + CoursewareCompanionBox, + Datepicker, + }, + props: { + structuralElement: Object, + }, + data() { + return { + permissionType: 'all', + visible: 'always', + visibleAll: false, + visibleApprovalUsers: [], + visibleApprovalGroups: [], + visibleStartDate: null, + visibleEndDate: null, + writable: 'never', + writableAll: false, + writableStartDate: null, + writableEndDate: null, + writableApprovalUsers: [], + writableApprovalGroups: [], + height: '680', + width: '870', + currentSemester: null, + }; + }, + computed: { + ...mapGetters({ + blocked: 'currentElementBlocked', + blockerId: 'currentElementBlockerId', + blockedByAnotherUser: 'currentElementBlockedByAnotherUser', + context: 'context', + relatedCourseMemberships: 'course-memberships/related', + relatedCourseStatusGroups: 'status-groups/related', + relatedUser: 'users/related', + userById: 'users/byId', + }), + blockingUser() { + if (this.blockedByAnotherUser) { + return this.userById({ id: this.blockerId }); + } + + return null; + }, + blockingUserName() { + return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : ''; + }, + users() { + const parent = { type: 'courses', id: this.context.id }; + const relationship = 'memberships'; + const memberships = this.relatedCourseMemberships({ parent, relationship }); + + return ( + memberships?.map((membership) => { + const parent = { type: membership.type, id: membership.id }; + const member = this.relatedUser({ parent, relationship: 'user' }); + + return { + id: member.id, + formattedname: member.attributes['formatted-name'], + username: member.attributes['username'], + perm: membership.attributes['permission'], + }; + }) ?? [] + ); + }, + statusGroupsUrl() { + return STUDIP.URLHelper.getURL('dispatch.php/course/statusgroups'); + }, + autorMembers() { + if (Object.keys(this.users).length === 0 && this.users.constructor === Object) { + return []; + } + + const members = this.users.filter(function (user) { + return user.perm === 'autor'; + }) ?? []; + + return members; + }, + groups() { + const parent = { type: 'courses', id: this.context.id }; + const relationship = 'status-groups'; + const statusGroups = this.relatedCourseStatusGroups({ parent, relationship }); + + return ( + statusGroups?.map((statusGroup) => { + return { + id: statusGroup.id, + name: statusGroup.attributes['name'], + }; + }) ?? [] + ); + }, + periodsValid() { + if (this.writable !== 'period' || this.visible !== 'period') { + return true; + } + return this.visibleStartDate <= this.writableStartDate + && ( + this.visibleEndDate === null + || this.visibleEndDate >= this.writableEndDate + ); + }, + semesterDates() { + const date = Date.now() / 1000; + let startDate = date; + let endDate = date; + if (this.currentSemester) { + startDate = new Date(this.currentSemester.attributes.start).getTime() / 1000; + endDate = new Date(this.currentSemester.attributes.end).getTime() / 1000; + } + + return { start: startDate, end: endDate }; + }, + dialogTitle() { + return this.$gettext('Rechte und Sichtbarkeit') + ': ' + this.structuralElement.attributes.title; + } + }, + methods: { + ...mapActions({ + loadCourseMemberships: 'course-memberships/loadRelated', + loadCourseStatusGroups: 'status-groups/loadRelated', + updateStructuralElement: 'updateStructuralElement', + loadStructuralElement: 'loadStructuralElement', + companionWarning: 'companionWarning', + unlockObject: 'unlockObject', + showStructuralElementPermissionsDialog: 'showStructuralElementPermissionsDialog', + }), + setDimensions() { + this.height = Math.min((window.innerHeight * 0.8).toFixed(0), 680).toString(); + this.width = Math.min((window.innerWidth * 0.8).toFixed(0), 870) + .toFixed(0) + .toString(); + }, + initData() { + this.permissionType = this.structuralElement.attributes['permission-type']; + this.visible = this.structuralElement.attributes['visible']; + this.visibleAll = this.structuralElement.attributes['visible-all']; + this.visibleStartDate = this.structuralElement.attributes['visible-start-date'] + ? new Date(this.structuralElement.attributes['visible-start-date']).getTime() / 1000 + : null; + this.visibleEndDate = this.structuralElement.attributes['visible-end-date'] + ? new Date(this.structuralElement.attributes['visible-end-date']).getTime() / 1000 + : null; + this.writable = this.structuralElement.attributes['writable']; + this.writableAll = this.structuralElement.attributes['writable-all']; + this.writableStartDate = this.structuralElement.attributes['writable-start-date'] + ? new Date(this.structuralElement.attributes['writable-start-date']).getTime() / 1000 + : null; + this.writableEndDate = this.structuralElement.attributes['writable-end-date'] + ? new Date(this.structuralElement.attributes['writable-end-date']).getTime() / 1000 + : null; + if (this.permissionType === 'users') { + this.visibleApprovalUsers = this.structuralElement.attributes['visible-approval']; + this.writableApprovalUsers = this.structuralElement.attributes['writable-approval']; + } + if (this.permissionType === 'groups') { + this.visibleApprovalGroups = this.structuralElement.attributes['visible-approval']; + this.writableApprovalGroups = this.structuralElement.attributes['writable-approval']; + } + + axios + .get(STUDIP.URLHelper.getURL('jsonapi.php/v1/semesters', { 'filter[current]': true }, true)) + .then((response) => { + this.currentSemester = response.data.data[0]; + }) + .catch((error) => { + this.currentSemester = null; + }); + }, + async storePermissions() { + await this.loadStructuralElement(this.structuralElement.id); + if (this.blockedByAnotherUser) { + this.companionWarning({ + info: this.$gettextInterpolate( + this.$gettext( + 'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.' + ), + { blockingUserName: this.blockingUserName } + ), + }); + this.$emit('close'); + return false; + } + if (!this.blocked) { + await this.lockObject({ id: this.structuralElement.id, type: 'courseware-structural-elements' }); + } + + let visibleApproval = []; + let writableApproval = []; + if (this.permissionType === 'users') { + visibleApproval = this.visibleApprovalUsers; + writableApproval = this.writableApprovalUsers; + } + if (this.permissionType === 'groups') { + visibleApproval = this.visibleApprovalGroups; + writableApproval = this.writableApprovalGroups; + } + + if (this.visible === 'period' && this.visibleStartDate === null && this.visibleEndDate === null) { + this.visible = 'always'; + } + + if (this.writable === 'period' && this.writableStartDate === null && this.writableEndDate === null) { + this.visible = 'always'; + } + + if ( + this.visible === 'period' && + this.visibleStartDate !== null && + this.visibleEndDate !== null && + this.visibleStartDate > this.visibleEndDate + ) { + this.companionWarning({ + info: this.$gettext( + 'Das Enddatum des Sichtbarkeitszeitraums darf nicht vor dem Startdatum liegen.' + ), + }); + return false; + } + + if ( + this.writable === 'period' && + this.writableStartDate !== null && + this.writableEndDate !== null && + this.writableStartDate > this.writableEndDate + ) { + this.companionWarning({ + info: this.$gettext('Das Enddatum des Bearbeitungszeitraums darf nicht vor dem Startdatum liegen.'), + }); + return false; + } + + if (!this.periodsValid) { + this.companionWarning({ + info: this.$gettext('Der Bearbeitungszeitraum muss innerhalb des Sichtbarkeitszeitraums liegen.'), + }); + return false; + } + + const structuralElement = { + id: this.structuralElement.id, + type: 'courseware-structural-elements', + attributes: { + 'permission-type': this.permissionType, + visible: this.visible, + 'visible-all': this.visibleAll && this.permissionType !== 'all' ? 1 : 0, + 'visible-start-date': + this.visible === 'period' ? new Date(this.visibleStartDate * 1000).toISOString() : null, + 'visible-end-date': + this.visible === 'period' ? new Date(this.visibleEndDate * 1000).toISOString() : null, + 'visible-approval': JSON.stringify(visibleApproval), + writable: this.writable, + 'writable-all': this.writableAll && this.permissionType !== 'all' ? 1 : 0, + 'writable-start-date': + this.writable === 'period' ? new Date(this.writableStartDate * 1000).toISOString() : null, + 'writable-end-date': + this.writable === 'period' ? new Date(this.writableEndDate * 1000).toISOString() : null, + 'writable-approval': JSON.stringify(writableApproval), + }, + }; + this.showStructuralElementPermissionsDialog(false); + await this.updateStructuralElement({ element: structuralElement, id: this.structuralElement.id }); + await this.unlockObject({ id: this.structuralElement.id, type: 'courseware-structural-elements' }); + this.$emit('store'); + }, + updatePermissionType() { + if (this.permissionType !== 'all') { + if (this.visible === 'never') { + this.visible = 'always'; + } + if (this.writable === 'never') { + this.writable = 'always'; + } + } else { + if (this.writable === 'always') { + this.writable = 'never'; + } + } + }, + updateVisibile() { + if (this.visible === 'never' && this.permissionType === 'all') { + this.writable = 'never'; + } + if (this.visible === 'period') { + if (this.writable === 'always') { + this.writable = 'period'; + this.writableStartDate = this.writableStartDate ?? this.semesterDates.start; + this.writableEndDate = this.writableEndDate ?? this.semesterDates.end; + } + + this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start; + this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end; + } + }, + updateWritable() { + if (this.writable === 'always') { + this.visible = 'always'; + } + if (this.writable === 'period' && this.permissionType === 'all' && this.visible !== 'always') { + this.visible = 'period'; + this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start; + this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end; + } + if (this.writable === 'period') { + this.writableStartDate = this.writableStartDate ?? this.semesterDates.start; + this.writableEndDate = this.writableEndDate ?? this.semesterDates.end; + } + }, + updateUserWritable(user) { + if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) { + this.visibleApprovalUsers.push(user.id); + } + }, + updateUserVisible(user) { + if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) { + this.writableApprovalUsers = this.writableApprovalUsers.filter((id) => id !== user.id); + } + }, + + updateGroupWritable(group) { + if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) { + this.visibleApprovalGroups.push(group.id); + } + }, + updateGroupVisible(group) { + if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) { + this.writableApprovalGroups = this.writableApprovalGroups.filter((id) => id !== group.id); + } + }, + updateVisibleAll() { + if (this.writableAll) { + this.visibleAll = true; + } + }, + updatewritableAll() { + if (!this.visibleAll) { + this.writableAll = false; + } + }, + }, + mounted() { + this.setDimensions(); + this.initData(); + const parent = { type: 'courses', id: this.context.id }; + let options = { + include: 'user', + 'page[limit]': 10000, + }; + this.loadCourseMemberships({ parent, relationship: 'memberships', options: options }); + this.loadCourseStatusGroups({ parent, relationship: 'status-groups' }); + }, +}; +</script> diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue new file mode 100644 index 0000000000000000000000000000000000000000..37a32228f5c1bef6a284925a47f0d016d44afeef --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue @@ -0,0 +1,84 @@ +<template> + <studip-dialog + :title="$gettext('Öffentlichen Link für Seite erzeugen')" + :confirmText="$gettext('Erstellen')" + confirmClass="accept" + :closeText="$gettext('Abbrechen')" + closeClass="cancel" + class="cw-structural-element-dialog" + @close="closePublicLinkDialog" + @confirm="createElementPublicLink" + > + <template v-slot:dialogContent> + <form class="default" @submit.prevent=""> + <label> + {{ $gettext('Passwort') }} + <input type="password" v-model="publicLink.password" /> + </label> + <label> + {{ $gettext('Ablaufdatum') }} + <datepicker v-model="publicLink['expire-date']" /> + </label> + </form> + </template> + </studip-dialog> +</template> +<script> +import Datepicker from './../../Datepicker.vue'; +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-structural-element-dialog-public-link', + components: { + Datepicker, + }, + props: { + structuralElement: Object, + }, + data() { + return { + publicLink: { + passsword: '', + 'expire-date': null + }, + }; + }, + methods: { + ...mapActions({ + companionSuccess: 'companionSuccess', + createLink: 'createLink', + showElementPublicLinkDialog: 'showElementPublicLinkDialog', + }), + async createElementPublicLink() { + const date = this.publicLink['expire-date']; + const publicLink = { + attributes: { + password: this.publicLink.password, + 'expire-date': date === null ? new Date(0).toISOString() : new Date(date * 1000).toISOString() + }, + relationships: { + 'structural-element': { + data: { + id: this.structuralElement.id, + type: 'courseware-structural-elements' + } + } + } + } + + await this.createLink({ publicLink }); + this.companionSuccess({ + info: this.$gettext('Öffentlicher Link wurde angelegt. Unter „Freigaben“ finden Sie alle Ihre öffentlichen Links.'), + }); + this.closePublicLinkDialog(); + }, + closePublicLinkDialog() { + this.publicLink = { + passsword: '', + 'expire-date': '' + }; + this.showElementPublicLinkDialog(false); + }, + } +}; +</script> diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogSettings.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogSettings.vue new file mode 100644 index 0000000000000000000000000000000000000000..dd81d541a86804169406729b739db05759194d40 --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogSettings.vue @@ -0,0 +1,342 @@ +<template> + <studip-dialog + :title="$gettext('Seiteneinstellungen')" + :confirmText="$gettext('Speichern')" + confirmClass="accept" + :closeText="$gettext('Schließen')" + closeClass="cancel" + height="560" + :width="inContent ? '720' : '500'" + class="studip-dialog-with-tab" + @close="$emit('close')" + @confirm="storeCurrentElement" + > + <template v-slot:dialogContent> + <courseware-tabs class="cw-tab-in-dialog"> + <courseware-tab :name="$gettext('Grunddaten')" :selected="true" :index="0"> + <form class="default" @submit.prevent=""> + <label> + {{ $gettext('Titel') }} + <input type="text" v-model="currentElement.attributes.title" /> + </label> + <label> + {{ $gettext('Beschreibung') }} + <textarea + v-model="currentElement.attributes.payload.description" + class="cw-structural-element-description" + ></textarea> + </label> + </form> + </courseware-tab> + <courseware-tab :name="$gettext('Metadaten')" :index="1"> + <form class="default" @submit.prevent=""> + <label> + {{ $gettext('Farbe') }} + <studip-select + v-model="currentElement.attributes.payload.color" + :options="colors" + :reduce="(color) => color.class" + label="class" + class="cw-vs-select" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span> + </template> + <template #no-options> + {{ $gettext('Es steht keine Auswahl zur Verfügung') }}. + </template> + <template #selected-option="{ name, hex }"> + <span class="vs__option-color" :style="{ 'background-color': hex }"></span + ><span>{{ name }}</span> + </template> + <template #option="{ name, hex }"> + <span class="vs__option-color" :style="{ 'background-color': hex }"></span + ><span>{{ name }}</span> + </template> + </studip-select> + </label> + <label> + {{ $gettext('Art des Lernmaterials') }} + <select v-model="currentElement.attributes.purpose"> + <option value="content">{{ $gettext('Inhalt') }}</option> + <option v-if="!inCourse" value="template"> + {{ $gettext('Aufgabenvorlage') }} + </option> + <option value="oer">{{ $gettext('OER-Material') }}</option> + <option value="portfolio">{{ $gettext('ePortfolio') }}</option> + <option value="draft">{{ $gettext('Entwurf') }}</option> + <option value="other">{{ $gettext('Sonstiges') }}</option> + </select> + </label> + <template v-if="currentElement.attributes.purpose === 'oer'"> + <label> + {{ $gettext('Lizenztyp') }} + <select v-model="currentElement.attributes.payload.license_type"> + <option v-for="license in licenses" :key="license.id" :value="license.id"> + {{ license.name }} + </option> + </select> + </label> + <label> + {{ $gettext('Geschätzter zeitlicher Aufwand') }} + <input type="text" v-model="currentElement.attributes.payload.required_time" /> + </label> + <label> + {{ $gettext('Niveau') }}<br /> + {{ $gettext('von') }} + <select v-model="currentElement.attributes.payload.difficulty_start"> + <option + v-for="difficulty_start in 12" + :key="difficulty_start" + :value="difficulty_start" + > + {{ difficulty_start }} + </option> + </select> + {{ $gettext('bis') }} + <select v-model="currentElement.attributes.payload.difficulty_end"> + <option v-for="difficulty_end in 12" :key="difficulty_end" :value="difficulty_end"> + {{ difficulty_end }} + </option> + </select> + </label> + </template> + </form> + </courseware-tab> + <courseware-tab :name="$gettext('Bild')" :index="2"> + <form class="default" @submit.prevent=""> + <template v-if="hasImage"> + <img + :src="image" + class="cw-structural-element-image-preview" + :alt="$gettext('Vorschaubild')" + /> + <label> + <button class="button" @click="deleteImage" v-translate>Bild löschen</button> + </label> + </template> + + <div v-else class="cw-structural-element-image-preview-placeholder"></div> + + <div v-if="uploadFileError" class="messagebox messagebox_error"> + {{ uploadFileError }} + </div> + + <div v-show="!hasImage"> + <label> + {{ $gettext('Bild hochladen') }} + <input + class="cw-file-input" + ref="upload_image" + type="file" + accept="image/*" + @change="checkUploadFile" + /> + </label> + {{ $gettext('oder') }} + <br /> + <button class="button" type="button" @click="showStockImageSelector = true"> + {{ $gettext('Aus dem Bilderpool auswählen') }} + </button> + <StockImageSelector + v-if="showStockImageSelector" + @close="showStockImageSelector = false" + @select="onSelectStockImage" + /> + </div> + </form> + </courseware-tab> + <courseware-tab v-if="inContent" :name="$gettext('Rechte')" :index="3"> + <courseware-content-permissions + :element="currentElement" + @updateContentApproval="updateContentApproval" + /> + </courseware-tab> + </courseware-tabs> + </template> + </studip-dialog> +</template> + +<script> +import CoursewareContentPermissions from '../CoursewareContentPermissions.vue'; +import CoursewareTabs from '../layouts/CoursewareTabs.vue'; +import CoursewareTab from '../layouts/CoursewareTab.vue'; +import wizardMixin from '@/vue/mixins/courseware/wizard.js'; +import colorMixin from '@/vue/mixins/courseware/colors.js'; +import StockImageSelector from '../../stock-images/SelectorDialog.vue'; +import { mapActions, mapGetters } from 'vuex'; +export default { + name: 'courseware-structural-element-dialog-settings', + mixins: [colorMixin, wizardMixin], + components: { + CoursewareContentPermissions, + CoursewareTabs, + CoursewareTab, + StockImageSelector, + }, + props: { + structuralElement: Object, + }, + data() { + return { + currentElement: _.cloneDeep(this.structuralElement), + showStockImageSelector: false, + selectedStockImage: null, + uploadFileError: '', + uploadImageURL: null, + deletingPreviewImage: false, + }; + }, + computed: { + ...mapGetters({ + blocked: 'currentElementBlocked', + blockerId: 'currentElementBlockerId', + blockedByThisUser: 'currentElementBlockedByThisUser', + blockedByAnotherUser: 'currentElementBlockedByAnotherUser', + context: 'context', + userId: 'userId', + userById: 'users/byId', + userIsTeacher: 'userIsTeacher', + }), + inCourse() { + return this.context.type === 'courses'; + }, + inContent() { + return this.context.type === 'users' && this.userId === this.structuralElement?.relationships.user.data.id; + }, + isTask() { + return this.structuralElement?.relationships.task.data !== null; + }, + currentId() { + return this.structuralElement?.id; + }, + colors() { + return this.mixinColors.filter(color => color.darkmode); + }, + blockingUser() { + if (this.blockedByAnotherUser) { + return this.userById({ id: this.blockerId }); + } + + return null; + }, + blockingUserName() { + return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : ''; + }, + hasImage() { + return (this.image || this.selectedStockImage) && this.deletingPreviewImage === false; + }, + image() { + if (this.selectedStockImage) { + return this.selectedStockImage.attributes['download-urls'].small; + } + if (this.uploadImageURL) { + return this.uploadImageURL; + } + return this.structuralElement.relationships?.image?.meta?.['download-url'] ?? null; + }, + + imageType() { + return this.structuralElement.relationships?.image?.data?.type ?? null; + }, + }, + methods: { + ...mapActions({ + deleteImageForStructuralElement: 'deleteImageForStructuralElement', + loadStructuralElement: 'loadStructuralElement', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + setStockImageForStructuralElement: 'setStockImageForStructuralElement', + showElementEditDialog: 'showElementEditDialog', + updateStructuralElement: 'updateStructuralElement', + uploadImageForStructuralElement: 'uploadImageForStructuralElement', + }), + checkUploadFile() { + const file = this.$refs?.upload_image?.files[0]; + this.uploadImageURL = null; + this.uploadFileError = this.checkUploadImageFile(this.$refs?.upload_image?.files[0]); + if (this.uploadFileError === '') { + this.deletingPreviewImage = false; + this.uploadImageURL = window.URL.createObjectURL(file); + } + }, + deleteImage() { + if (!this.deletingPreviewImage) { + this.deletingPreviewImage = true; + } + }, + onSelectStockImage(stockImage) { + if (this.$refs?.upload_image) { + this.$refs.upload_image.value = null; + } + this.selectedStockImage = stockImage; + this.showStockImageSelector = false; + this.deletingPreviewImage = false; + }, + updateContentApproval(approval) { + this.currentElement.attributes['content-approval'] = approval; + }, + async storeCurrentElement() { + await this.loadStructuralElement(this.currentElement.id); + if (this.blockedByAnotherUser) { + this.companionWarning({ + info: this.$gettextInterpolate( + this.$gettext( + 'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.' + ), + { blockingUserName: this.blockingUserName } + ), + }); + this.showElementEditDialog(false); + return false; + } + if (!this.blocked) { + await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + } + + const file = this.$refs?.upload_image?.files[0]; + try { + this.uploadFileError = ''; + if (file) { + await this.uploadImageForStructuralElement({ + structuralElement: this.currentElement, + file, + }); + } else if (this.selectedStockImage) { + await this.setStockImageForStructuralElement({ + structuralElement: this.currentElement, + stockImage: this.selectedStockImage, + }); + } else if (this.deletingPreviewImage) { + await this.deleteImageForStructuralElement(this.currentElement); + } + + this.loadStructuralElement(this.currentElement.id); + } catch (error) { + console.error(error); + this.uploadFileError = this.$gettext( + 'Das Bild für das neue Lernmaterial konnte nicht gespeichert werden.' + ); + } + + this.showElementEditDialog(false); + + const element = { + id: this.currentElement.id, + type: this.currentElement.type, + attributes: this.currentElement.attributes, + }; + + await this.updateStructuralElement({ element, id: this.currentId }); + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.$emit('store'); + }, + }, + mounted() { + // this.currentElement = _.cloneDeep(this.structuralElement); + this.uploadFileError = ''; + this.deletingPreviewImage = false; + this.uploadImageURL = null; + }, +}; +</script> diff --git a/resources/vue/components/courseware/structural-element/CoursewareTree.vue b/resources/vue/components/courseware/structural-element/CoursewareTree.vue index 08019dd26d7d53661b6fbaa2cdc670714cba653c..bd25c6b66900169db242632d07dd355993c27231 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareTree.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareTree.vue @@ -57,7 +57,7 @@ export default { childrenById: 'courseware-structure/children', viewMode: 'viewMode', structuralElements: 'courseware-structural-elements/all', - assistiveLive: 'assistiveLiveContents' + assistiveLive: 'assistiveLiveContents', }), currentElement() { const id = this.$route?.params?.id; @@ -94,6 +94,8 @@ export default { loadStructuralElement: 'loadStructuralElement', setAssistiveLiveContents: 'setAssistiveLiveContents', companionError: 'companionError', + loadCourseMemberships: 'course-memberships/loadRelated', + loadCourseStatusGroups: 'status-groups/loadRelated', }), updateNestedChildren() { this.nestedChildren = this.getNestedChildren(this.rootElement); @@ -241,6 +243,9 @@ export default { }, mounted() { this.updateNestedChildren(); + const parent = { type: 'courses', id: this.context.id }; + this.loadCourseMemberships({ parent, relationship: 'memberships', options: {'page[limit]': 10000 } }); + this.loadCourseStatusGroups({ parent, relationship: 'status-groups' }); }, watch: { structuralElements() { diff --git a/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue b/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue index 540e76b3529911592163b77db781b1679815b1bf..6fe7da87d39c4e872eb5090d8fb1f63f21ffbff4 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue @@ -36,18 +36,14 @@ <span v-if="hasReleaseOrWithdrawDate" class="cw-tree-item-flag-date" - :title="$gettext('Diese Seite hat eine zeitlich beschränkte Sichtbarkeit')" + :title="visibleStartEndDate" ></span> <span v-if="hasWriteApproval" class="cw-tree-item-flag-write" - :title="$gettext('Diese Seite kann von Teilnehmenden bearbeitet werden')" - ></span> - <span - v-if="hasNoReadApproval" - class="cw-tree-item-flag-cant-read" - :title="$gettext('Diese Seite kann von Teilnehmenden nicht gesehen werden')" + :title="canWriteFlagTitle" ></span> + <span v-if="hasNoReadApproval" class="cw-tree-item-flag-cant-read" :title="cantReadFlagTitle"></span> <template v-if="!userIsTeacher && inCourse"> <span v-if="complete" @@ -113,10 +109,7 @@ @childrenUpdated="$emit('childrenUpdated')" /> </draggable> - <ol - v-if="canEdit && isFirstLevel" - class="cw-tree-adder-list" - > + <ol v-if="canEdit && isFirstLevel" class="cw-tree-adder-list"> <courseware-tree-item-adder :parentId="element.id" /> </ol> </li> @@ -180,8 +173,23 @@ export default { courseware: 'courseware', progressData: 'progresses', userIsTeacher: 'userIsTeacher', - showRootElement: 'showRootElement' + showRootElement: 'showRootElement', + relatedCourseMemberships: 'course-memberships/related', + relatedCourseStatusGroups: 'status-groups/related', }), + autorMembersCount() { + // course-memberships are loaded in parent! + const parent = { type: 'courses', id: this.context.id }; + const relationship = 'memberships'; + const memberships = this.relatedCourseMemberships({ parent, relationship }) ?? []; + return memberships.filter(m => m.attributes.permission === 'autor').length + }, + statusGroupsCount() { + // status-groups are loaded in parent! + const parent = { type: 'courses', id: this.context.id }; + const relationship = 'status-groups'; + return this.relatedCourseStatusGroups({ parent, relationship }).length; + }, draggableData() { return { attrs: { @@ -218,32 +226,131 @@ export default { return this.element.id === this.currentElement?.id; }, hasReleaseOrWithdrawDate() { - return ( - this.element.attributes?.['release-date'] !== null || - this.element.attributes?.['withdraw-date'] !== null - ); + return this.element.attributes?.visible === 'period'; + }, + visibleStartEndDate() { + if (this.hasReleaseOrWithdrawDate) { + const startDate = STUDIP.DateTime.getStudipDate( + new Date(this.element.attributes?.['visible-start-date']), + false, + true + ); + const endDate = STUDIP.DateTime.getStudipDate( + new Date(this.element.attributes?.['visible-end-date']), + false, + true + ); + let persons = ''; + switch (this.element.attributes?.['permission-type']) { + case 'all': + persons = this.$gettext('alle'); + break; + case 'users': { + const users = this.element.attributes['visible-approval'].length; + persons = this.$gettextInterpolate( + this.$ngettext('einen Studierenden', '%{count} Studierende', users), + { count: users } + ); + break; + } + case 'groups': { + const groups = this.element.attributes['visible-approval'].length; + persons = this.$gettextInterpolate(this.$ngettext('eine Gruppe', '%{count} Gruppen', groups), { + count: groups, + }); + break; + } + } + + return this.$gettextInterpolate( + this.$gettext('Diese Seite ist vom %{start} bis zum %{end} für %{persons} sichtbar'), + { start: startDate, end: endDate, persons: persons } + ); + } + + return ''; }, hasWriteApproval() { - const writeApproval = this.element.attributes?.['write-approval']; + if (this.element.attributes?.['permission-type'] === 'all') { + return this.element.attributes?.writable !== 'never'; + } - if (!writeApproval || Object.keys(writeApproval).length === 0) { - return false; + if (this.element.attributes?.['writable-all']) { + return true; } - return ( - (writeApproval.all || writeApproval.groups.length > 0 || writeApproval.users.length > 0) && - this.element.attributes?.['can-edit'] - ); + + const writableApproval = this.element?.attributes?.['writable-approval'] ?? []; + return writableApproval.length !== 0; + }, + canWriteFlagTitle() { + if (this.element.attributes?.['writable-all'] || this.element.attributes?.['permission-type'] === 'all') { + return this.$gettext('Diese Seite kann von allen Studierenden bearbeitet werden'); + } + let persons = ''; + const writableApproval = this.element?.attributes?.['writable-approval'] ?? []; + const count = writableApproval.length; + switch (this.element.attributes?.['permission-type']) { + case 'users': + persons = this.$gettextInterpolate(this.$ngettext('einem Studierendem', '%{count} Studierenden', count), { + count: count, + }); + break; + case 'groups': + persons = this.$gettextInterpolate(this.$ngettext('einer Gruppe', '%{count} Gruppen', count), { + count: count, + }); + break; + } + + return this.$gettextInterpolate( + this.$gettext('Diese Seite kann von %{persons} bearbeitet werden'), + { persons: persons } + ); }, hasNoReadApproval() { - if (this.context.type === 'users') { + if (this.context.type === 'users' || this.element.attributes?.['visible-all']) { return false; } - const readApproval = this.element.attributes?.['read-approval']; + const visibleApproval = this.element?.attributes?.['visible-approval'] ?? []; + switch (this.element.attributes?.['permission-type']) { + case 'all': + return this.element.attributes?.visible === 'never'; + case 'users': + return this.autorMembersCount !== visibleApproval.length; + case 'groups': + return this.statusGroupsCount !== visibleApproval.length; + } - if (!readApproval || Object.keys(readApproval).length === 0 || this.hasWriteApproval) { - return false; + return true; + }, + cantReadFlagTitle() { + if (!this.hasNoReadApproval) { + return ''; } - return !readApproval.all && readApproval.groups.length === 0 && readApproval.users.length === 0; + const visibleApproval = this.element?.attributes?.['visible-approval'] ?? []; + let persons = ''; + let count = 0; + switch (this.element.attributes?.['permission-type']) { + case 'all': + return this.$gettext('Diese Seite kann von Studierenden nicht gesehen werden'); + case 'users': + count = this.autorMembersCount - visibleApproval.length; + persons = this.$gettextInterpolate(this.$ngettext('einem Studierendem', '%{count} Studierenden', count), { + count: count, + }); + break; + case 'groups': + count = this.statusGroupsCount - visibleApproval.length + persons = this.$gettextInterpolate(this.$ngettext('einer Gruppe', '%{count} Gruppen', count), { + count: count, + }); + break; + } + + return this.$gettextInterpolate( + this.$gettext('Diese Seite kann von %{persons} nicht gesehen werden'), + { persons: persons } + ); }, hasPurposeClass() { return this.purposeClass !== ''; diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue index e0299968a62bcacbcea165056ec4a3713a1e4231..c7447cea5855cabfb244de836dc7f515b6f84151 100644 --- a/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue +++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue @@ -295,7 +295,8 @@ export default { color: this.modifiedColor, description: this.modifiedDescription !== '' ? this.modifiedDescription : this.selectedUnitDescription } - await this.copyUnit({ unitId: this.selectedUnit.id, modified: modified }); + const duplicate = (this.source === 'self' && this.context.type === 'courses') || (this.source === 'users' && this.context.type === 'users') + await this.copyUnit({ unitId: this.selectedUnit.id, modified: modified, duplicate: duplicate }); this.companionSuccess({ info: this.$gettext('Lernmaterial kopiert.') }); this.close(); } diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue index dc68225f87350317705a4dbc73572868b5e28715..f2c4d4a73f589c8eaa1cd47903cb0ac194cce25a 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue @@ -24,36 +24,50 @@ @showProgress="openProgressDialog" @showSettings="openSettingsDialog" @showLayout="openLayoutDialog" - @copyUnit="copy" + @showPermissions="openPermissionsDialog" + @duplicateUnit="duplicate" @showFeedbackCreate="openFeedbackCreateDialog" @showFeedback="openFeedbackDialog" /> </template> + <template #image-overlay-bottom v-if="hasFeedbackElement"> + <studip-five-stars + v-if="hasFeedbackEntries" + :amount="feedbackAverage" + :size="16" + :title=" + $gettextInterpolate($gettext('Lernmaterial wurde mit %{avg} Sternen bewertet'), { + avg: feedbackAverage, + }) + " + /> + <studip-five-stars + v-else + :amount="5" + :size="16" + role="inactive" + :title="$gettext('Lernmaterial wurde noch nicht bewertet')" + /> + </template> <template #description> {{ description }} </template> <template #footer> - <template v-if="hasFeedbackElement"> - <studip-five-stars - v-if="hasFeedbackEntries" - :amount="feedbackAverage" - :size="16" - :title=" - $gettextInterpolate($gettext('Lernmaterial wurde mit %{avg} Sternen bewertet'), { - avg: feedbackAverage, - }) - " - /> - <studip-five-stars - v-else - :amount="5" - :size="16" - role="inactive" - :title="$gettext('Lernmaterial wurde noch nicht bewertet')" - /> + <template v-if="hasPermissionSettings"> + <p v-if="visiblePermissionInfo" :title="visiblePermissionInfo?.title"> + <studip-icon :shape="visiblePermissionInfo.icon" role="info_alt" :size="16" /> + {{ visiblePermissionInfo.text }} + </p> + <p v-if="writablePermissionInfo" :title="writablePermissionInfo?.title"> + <studip-icon :shape="writablePermissionInfo.icon" role="info_alt" :size="16" /> + {{ writablePermissionInfo.text }} + </p> </template> <template v-if="certificate"> - <studip-icon shape="medal" :size="16" role="info_alt" /> + <p> + <studip-icon shape="medal" :size="16" role="info_alt" /> + {{ $gettext('Zertifikat') }} + </p> </template> </template> </courseware-tile> @@ -96,6 +110,18 @@ :unitElement="unitElement" @close="closeLayoutDialog" /> + <courseware-unit-item-dialog-permission-scope + v-if="showPermissionScopeDialog" + :unit="unit" + @close="closePermissionsDialog" + @switch="switchPermissionScope" + /> + <courseware-unit-item-dialog-permissions + v-if="showPermissionSettingsDialog" + :unit="unit" + :unit-name="title" + @close="closePermissionsDialog" + /> <feedback-dialog v-if="showFeedbackDialog" :feedbackElementId="parseInt(feedbackElementId)" @@ -119,13 +145,14 @@ import CoursewareTile from '../layouts/CoursewareTile.vue'; import CoursewareUnitItemDialogExport from './CoursewareUnitItemDialogExport.vue'; import CoursewareUnitItemDialogSettings from './CoursewareUnitItemDialogSettings.vue'; import CoursewareUnitItemDialogLayout from './CoursewareUnitItemDialogLayout.vue'; +import CoursewareUnitItemDialogPermissions from './CoursewareUnitItemDialogPermissions.vue'; +import CoursewareUnitItemDialogPermissionScope from './CoursewareUnitItemDialogPermissionScope.vue'; import CoursewareUnitProgress from './CoursewareUnitProgress.vue'; import FeedbackDialog from '../../feedback/FeedbackDialog.vue'; import FeedbackCreateDialog from '../../feedback/FeedbackCreateDialog.vue'; import StudipFiveStars from '../../feedback/StudipFiveStars.vue'; import axios from 'axios'; - import { mapActions, mapGetters } from 'vuex'; export default { @@ -135,6 +162,8 @@ export default { CoursewareUnitItemDialogExport, CoursewareUnitItemDialogLayout, CoursewareUnitItemDialogSettings, + CoursewareUnitItemDialogPermissions, + CoursewareUnitItemDialogPermissionScope, CoursewareUnitProgress, FeedbackDialog, FeedbackCreateDialog, @@ -144,8 +173,8 @@ export default { unit: Object, handle: { type: Boolean, - default: true - } + default: true, + }, }, data() { return { @@ -154,10 +183,14 @@ export default { showSettingsDialog: false, showProgressDialog: false, showLayoutDialog: false, + showPermissionsDialog: false, progresses: null, certificate: null, showFeedbackDialog: false, showFeedbackCreateDialog: false, + + showPermissionScopeDialog: false, + showPermissionSettingsDialog: false, }; }, computed: { @@ -180,7 +213,7 @@ export default { if (this.isFeedbackActivated) { if (this.canCreateFeedbackElement && !this.hasFeedbackElement) { menu.push({ - id: 6, + id: 7, label: this.$gettext('Feedback aktivieren'), icon: 'feedback', emit: 'showFeedbackCreate', @@ -188,7 +221,7 @@ export default { } if (this.hasFeedbackElement) { menu.push({ - id: 6, + id: 7, label: this.$gettext('Feedback anzeigen'), icon: 'feedback', emit: 'showFeedback', @@ -203,17 +236,26 @@ export default { url: STUDIP.URLHelper.getURL('sendfile.php', { type: 0, file_id: this.certificate, - file_name: this.$gettext('Zertifikat') + '.pdf' - }) + file_name: this.$gettext('Zertifikat') + '.pdf', + }), }); } } if (this.userIsTeacher || !this.inCourseContext) { menu.push({ id: 4, label: this.$gettext('Darstellung'), icon: 'colorpicker', emit: 'showLayout' }); - menu.push({ id: 5, label: this.$gettext('Duplizieren'), icon: 'copy', emit: 'copyUnit' }); - menu.push({ id: 7, label: this.$gettext('Exportieren'), icon: 'export', emit: 'showExport' }); - menu.push({ id: 8, label: this.$gettext('Löschen'), icon: 'trash', emit: 'showDelete' }); + menu.push({ id: 6, label: this.$gettext('Duplizieren'), icon: 'copy', emit: 'duplicateUnit' }); + menu.push({ id: 8, label: this.$gettext('Exportieren'), icon: 'export', emit: 'showExport' }); + menu.push({ id: 9, label: this.$gettext('Löschen'), icon: 'trash', emit: 'showDelete' }); + } + + if (this.userIsTeacher && this.inCourseContext) { + menu.push({ + id: 5, + label: this.$gettext('Rechte & Sichtbarkeit'), + icon: 'lock-unlocked', + emit: 'showPermissions', + }); } menu.sort((a, b) => { @@ -269,6 +311,167 @@ export default { inCourseContext() { return this.context.type === 'courses'; }, + hasPermissionSettings() { + return this.unit.attributes['permission-scope'] === 'unit'; + }, + visiblePermissionInfo() { + if (!this.hasPermissionSettings) { + return false; + } + let info = { icon: '', text: '', title: '' }; + if (!this.userIsTeacher) { + if (this.unit.attributes.visible === 'period') { + info.icon = 'date'; + info.text = this.$gettextInterpolate(this.$gettext('Sichtbar bis zum %{end}'), { + end: this.permissionVisibleEndDate, + }); + + return info; + } + return false; + } + + switch (this.unit.attributes.visible) { + case 'always': + info.icon = 'visibility-visible'; + switch (this.unit.attributes['permission-type']) { + case 'all': + info.text = this.$gettext('Sichtbar für alle'); + break; + case 'users': { + if (this.unit.attributes['visible-all']) { + info.text = this.$gettext('Sichtbar für alle'); + } else { + const users = this.unit.attributes['visible-approval'].length; + info.text = this.$gettextInterpolate( + this.$ngettext( + 'Sichtbar für einen Studierenden', + 'Sichtbar für %{count} Studierende', + users + ), + { count: users } + ); + if (users === 0) { + info.icon = 'lock-locked'; + info.text = this.$gettext('Nur sichtbar für Lehrende'); + } + } + break; + } + case 'groups': { + if (this.unit.attributes['visible-all']) { + info.text = this.$gettext('Sichtbar für alle'); + } else { + const groups = this.unit.attributes['visible-approval'].length; + info.text = this.$gettextInterpolate( + this.$ngettext('Sichtbar für eine Gruppe', 'Sichtbar für %{count} Gruppen', groups), + { count: groups } + ); + if (groups === 0) { + info.icon = 'lock-locked'; + info.text = this.$gettext('Nur sichtbar für Lehrende'); + } + } + break; + } + } + break; + case 'never': + info.icon = 'lock-locked'; + info.text = this.$gettext('Nur sichtbar für Lehrende'); + break; + case 'period': { + info.icon = 'date'; + info.title = this.$gettextInterpolate( + this.$gettext('Für %{persons} sichtbar vom %{start} bis zum %{end}'), + { + start: this.permissionVisibleStartDate, + end: this.permissionVisibleEndDate, + persons: this.getPermissionPersons('visible-approval'), + } + ); + info.text = this.$gettext('Zeitlich begrenzt sichtbar'); + if ( + this.unit.attributes['permission-type'] !== 'all' && + this.unit.attributes['visible-approval'].length === 0 + ) { + info.icon = 'lock-locked'; + info.title = ''; + info.text = this.$gettext('Nur sichtbar für Lehrende'); + } + break; + } + } + + return info; + }, + permissionVisibleStartDate() { + return STUDIP.DateTime.getStudipDate(new Date(this.unit.attributes?.['visible-start-date']), false, true); + }, + permissionVisibleEndDate() { + return STUDIP.DateTime.getStudipDate(new Date(this.unit.attributes?.['visible-end-date']), false, true); + }, + permissionWritableStartDate() { + return STUDIP.DateTime.getStudipDate(new Date(this.unit.attributes?.['writable-start-date']), false, true); + }, + permissionWritableEndDate() { + return STUDIP.DateTime.getStudipDate(new Date(this.unit.attributes?.['writable-end-date']), false, true); + }, + writablePermissionInfo() { + if (this.unit.attributes['permission-scope'] !== 'unit') { + return false; + } + + let info = { icon: '', text: '', title: '' }; + + if (!this.userIsTeacher) { + if (this.unit.attributes['can-edit-content']) { + info.icon = 'edit'; + if (this.unit.attributes.writable === 'period') { + info.text = this.$gettextInterpolate(this.$gettext('Bearbeitbar bis zum %{end}'), { + end: this.permissionWritableEndDate, + }); + } else { + info.text = this.$gettext('Bearbeitbar'); + } + + return info; + } + + return false; + } + + if (['always', 'period'].includes(this.unit.attributes.writable)) { + if ( + this.unit.attributes['permission-type'] !== 'all' && + this.unit.attributes['writable-approval'].length === 0 && + !this.unit.attributes['writable-all'] + ) { + return false; + } + info.icon = 'edit'; + if (this.unit.attributes.writable === 'always') { + info.text = this.$gettextInterpolate(this.$gettext('Bearbeitbar für %{persons} '), { + persons: this.getPermissionPersons('writable-approval'), + }); + } + if (this.unit.attributes.writable === 'period') { + info.title = this.$gettextInterpolate( + this.$gettext('Für %{persons} bearbeitbar vom %{start} bis zum %{end}'), + { + start: this.permissionWritableStartDate, + end: this.permissionWritableEndDate, + persons: this.getPermissionPersons('writable-approval'), + } + ); + info.text = this.$gettext('Zeitlich begrenzt bearbeitbar'); + } + + return info; + } + + return false; + }, }, async mounted() { if (this.inCourseContext) { @@ -288,12 +491,16 @@ export default { }), checkCertificate() { if (this.getStudipConfig('COURSEWARE_CERTIFICATES_ENABLE') && this.unit.attributes.config.certificate) { - axios.get(STUDIP.URLHelper.getURL('jsonapi.php/v1/courseware-units/' + - this.unit.id + '/certificate/' + STUDIP.USER_ID)) - .then(response => { + axios + .get( + STUDIP.URLHelper.getURL( + 'jsonapi.php/v1/courseware-units/' + this.unit.id + '/certificate/' + STUDIP.USER_ID + ) + ) + .then((response) => { this.certificate = response.data; }) - .catch(error => {}); + .catch((error) => {}); } }, executeDelete() { @@ -327,6 +534,14 @@ export default { closeLayoutDialog() { this.showLayoutDialog = false; }, + openPermissionsDialog() { + this.showPermissionsDialog = true; + }, + closePermissionsDialog() { + this.showPermissionsDialog = false; + this.showPermissionScopeDialog = false; + this.showPermissionSettingsDialog = false; + }, openFeedbackCreateDialog() { this.showFeedbackCreateDialog = true; }, @@ -342,10 +557,57 @@ export default { this.showFeedbackDialog = false; this.loadFeedbackElement({ id: this.feedbackElementId }); }, - async copy() { - await this.copyUnit({ unitId: this.unit.id, modified: null }); + async duplicate() { + await this.copyUnit({ unitId: this.unit.id, modified: null, duplicate: true }); this.companionSuccess({ info: this.$gettext('Lernmaterial kopiert.') }); }, - } -} + async switchPermissionScope() { + await this.loadUnit({ id: this.unit.id }); + this.showPermissionScopeDialog = false; + this.showPermissionSettingsDialog = true; + }, + getPermissionPersons(type) { + switch (this.unit.attributes['permission-type']) { + case 'all': + return this.$gettext('alle'); + case 'users': { + if (this.unit.attributes['writable-all']) { + return this.$gettext('alle'); + } else { + const users = this.unit.attributes[type].length; + return this.$gettextInterpolate( + this.$ngettext('einen Studierenden', '%{count} Studierende', users), + { count: users } + ); + } + } + case 'groups': { + if (this.unit.attributes['writable-all']) { + return this.$gettext('alle'); + } else { + const groups = this.unit.attributes[type].length; + return this.$gettextInterpolate(this.$ngettext('eine Gruppe', '%{count} Gruppen', groups), { + count: groups, + }); + } + } + } + + return '-'; + }, + }, + watch: { + showPermissionsDialog(newVal) { + if (newVal) { + if (this.unit.attributes['permission-scope'] !== 'unit') { + this.showPermissionScopeDialog = true; + this.showPermissionSettingsDialog = false; + } else { + this.showPermissionScopeDialog = false; + this.showPermissionSettingsDialog = true; + } + } + }, + }, +}; </script> diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissionScope.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissionScope.vue new file mode 100644 index 0000000000000000000000000000000000000000..46a85c5c2cd9e0d86e5a3ab9ebe86c0caea65ec1 --- /dev/null +++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissionScope.vue @@ -0,0 +1,44 @@ +<template> + <studip-dialog + :title="$gettext('Rechte und Sichtbarkeit')" + :confirm-text="$gettext('Wechseln')" + confirm-class="accept" + :close-text="$gettext('Abbrechen')" + close-class="cancel" + :question="$gettext('Sie haben bereits die Rechte und Sichtbarkeit für einzelne Seiten eingestellt. Möchten Sie die Rechte für das gesamte Lernmaterial anpassen? Achtung: Die bereits an den einzelnen Seiten festgelegten Rechte werden überschrieben.')" + height="260" + @close="$emit('close')" + @confirm="switchPermissionScope" + > + </studip-dialog> +</template> +<script> +import { mapActions } from 'vuex'; + +export default { + name: 'courseware-unit-item-dialog-permission-scope', + props: { + unit: { + type: Object, + required: true, + }, + }, + methods: { + ...mapActions({ + updateUnit: 'courseware-units/update', + loadUnit: 'courseware-units/loadById', + }), + async switchPermissionScope() { + const unit = { + id: this.unit.id, + type: 'courseware-units', + attributes: { + 'permission-scope': 'unit', + }, + }; + await this.updateUnit(unit); + this.$emit('switch'); + }, + } +}; +</script> diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue new file mode 100644 index 0000000000000000000000000000000000000000..86ac64128522a83e2c1de77e8df88d87ee434bf3 --- /dev/null +++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue @@ -0,0 +1,585 @@ +<template> + <studip-dialog + :title="dialogTitle" + :confirm-text="$gettext('Speichern')" + confirm-class="accept" + :close-text="$gettext('Schließen')" + close-class="cancel" + :height="height" + :width="width" + @close="$emit('close')" + @confirm="storePermissions" + > + <template v-slot:dialogContent> + <div class="cw-permissions-form-wrapper"> + <form class="default cw-permissions-form-radioset" @submit.prevent=""> + <div class="cw-radioset-wrapper" role="group" aria-labelledby="permission-type"> + <p class="sr-only" id="permission-type">{{ $gettext('Typ') }}</p> + <div class="cw-radioset"> + <div class="cw-radioset-box" :class="[permissionType === 'all' ? 'selected' : '']"> + <input + type="radio" + id="permission-type-all" + value="all" + v-model="permissionType" + @change="updatePermissionType" + /> + <label for="permission-type-all"> + <div + class="label-icon all" + :class="[permissionType === 'all' ? 'selected' : '']" + ></div> + <div class="label-text"> + <span>{{ $gettext('alle Studierenden') }}</span> + </div> + </label> + </div> + <div class="cw-radioset-box" :class="[permissionType === 'users' ? 'selected' : '']"> + <input + type="radio" + id="permission-type-users" + value="users" + v-model="permissionType" + @change="updatePermissionType" + /> + <label for="permission-type-users"> + <div + class="label-icon users" + :class="[permissionType === 'users' ? 'selected' : '']" + ></div> + <div class="label-text"> + <span>{{ $gettext('ausgewählte Studierende') }}</span> + </div> + </label> + </div> + <div class="cw-radioset-box" :class="[permissionType === 'groups' ? 'selected' : '']"> + <input + type="radio" + id="permission-type-groups" + value="groups" + v-model="permissionType" + @change="updatePermissionType" + /> + <label for="permission-type-groups"> + <div + class="label-icon groups" + :class="[permissionType === 'groups' ? 'selected' : '']" + ></div> + <div class="label-text"> + <span>{{ $gettext('Gruppen') }}</span> + </div> + </label> + </div> + </div> + </div> + </form> + + <form class="default cw-form-selects" @submit.prevent=""> + <div class="cw-form-selects-row"> + <label> + {{ $gettext('Sichtbar') }} + <select v-model="visible" @change="updateVisibile"> + <option value="always">{{ $gettext('Immer') }}</option> + <option value="period">{{ $gettext('Zeitraum') }}</option> + <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option> + </select> + </label> + <template v-if="visible === 'period'"> + <label> + {{ $gettext('von') }} + <datepicker v-model="visibleStartDate" :placeholder="$gettext('unbegrenzt')" /> + </label> + <label> + {{ $gettext('bis') }} + <datepicker v-model="visibleEndDate" :placeholder="$gettext('unbegrenzt')" /> + </label> + </template> + </div> + <div class="cw-form-selects-row"> + <label + >{{ $gettext('Bearbeitbar') }} + <select v-model="writable" @change="updateWritable"> + <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option> + <option value="always">{{ $gettext('Immer') }}</option> + <option value="period">{{ $gettext('Zeitraum') }}</option> + </select> + </label> + <template v-if="writable === 'period'"> + <div> + <label> + {{ $gettext('von') }} + <datepicker v-model="writableStartDate" :placeholder="$gettext('unbegrenzt')" /> + </label> + </div> + <div> + <label> + {{ $gettext('bis') }} + <datepicker v-model="writableEndDate" :placeholder="$gettext('unbegrenzt')" /> + </label> + </div> + </template> + </div> + </form> + </div> + <div v-if="permissionType === 'all'" class="cw-contents-overview-teaser"> + <div class="cw-contents-overview-teaser-content"> + <header>{{ $gettext('Rechte und Sichtbarkeit') }}</header> + <p> + {{ + $gettext( + 'Hier stellen Sie für das gesamte Lernmaterial ein, welche Teilnehmenden aus Ihrer Veranstaltung alle Coursewareseiten dieses Materials sehen bzw. bearbeiten können. Falls Sie für Ihr Lehrszenario eine feinere Einstellungsmöglichkeit benötigen, können Sie direkt im Lernmaterial die „Rechte und Sichtbarkeit“ an den einzelnen Seiten einstellen.' + ) + }} + </p> + <p> + {{ + $gettext( + 'Entscheiden Sie sich zunächst ob „alle Studierende“ die gleichen Rechte erhalten sollen, oder ob „einzelne Studierende“ oder zuvor erstellte „Gruppen“ unterschiedliche Rechte benötigen. Die Einstellung „einzelne Studierende“ oder „Gruppen“ bietet sich beispielsweise dann an, wenn Sie eine Courseware von einer Kleingruppe bearbeiten lassen wollen. Anschließend können Sie einstellen, in welchem Zeitraum diese Rechte gelten.' + ) + }} + </p> + </div> + </div> + + <table v-if="permissionType === 'users'" class="default permission-table"> + <caption> + {{ $gettext('Studierende') }} + </caption> + <thead> + <tr> + <th>{{ $gettext('Name') }}</th> + <th> + {{ $gettext('Sichtbar') }} + <input type="checkbox" v-model="visibleAll" @change="updatewritableAll" /> + </th> + <th> + {{ $gettext('Bearbeitbar') }} + <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" /> + </th> + </tr> + </thead> + <tbody> + <tr v-if="autorMembers.length === 0"> + <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td> + </tr> + <tr v-for="autor in autorMembers" :key="autor.id"> + <td>{{ autor.formattedname }}</td> + <td> + <input + v-if="!visibleAll" + type="checkbox" + :value="autor.id" + v-model="visibleApprovalUsers" + @change="updateUserVisible(autor)" + /> + <studip-icon v-else shape="accept" role="info" :size="14" /> + </td> + <td> + <input + v-if="!writableAll" + type="checkbox" + :value="autor.id" + v-model="writableApprovalUsers" + @change="updateUserWritable(autor)" + /> + <studip-icon v-else shape="accept" role="info" :size="14" /> + </td> + </tr> + </tbody> + </table> + <template v-if="permissionType === 'groups'"> + <table v-if="groups.length > 0" class="default permission-table"> + <caption> + {{ $gettext('Gruppen') }} + </caption> + <thead> + <tr> + <th>{{ $gettext('Name') }}</th> + <th> + {{ $gettext('Sichtbar') }} + <input type="checkbox" v-model="visibleAll" :disabled="writableAll" /> + </th> + <th> + {{ $gettext('Bearbeitbar') }} + <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" /> + </th> + </tr> + </thead> + <tbody> + <tr v-if="groups.length === 0"> + <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td> + </tr> + <tr v-for="group in groups" :key="group.id"> + <td>{{ group.name }}</td> + <td> + <input + v-if="!visibleAll" + type="checkbox" + :value="group.id" + v-model="visibleApprovalGroups" + @change="updateGroupVisible(group)" + /> + <studip-icon v-else shape="accept" role="info" :size="14" /> + </td> + <td> + <input + v-if="!writableAll" + type="checkbox" + :value="group.id" + v-model="writableApprovalGroups" + @change="updateGroupWritable(group)" + /> + <studip-icon v-else shape="accept" role="info" :size="14" /> + </td> + </tr> + </tbody> + </table> + <courseware-companion-box + v-else + :msgCompanion="$gettext('Sie haben noch keine Gruppen erstellt. Mit Gruppen können Sie die Sichtbarkeits- und Bearbeitungsrechte anschließend besonders unkompliziert an Arbeitsgruppen vergeben.')" + mood="pointing" + > + <template #companionActions> + <a :href="statusGroupsUrl"><button class="button">{{ $gettext('Zu den Gruppen der Veranstaltung') }}</button></a> + </template> + </courseware-companion-box> + </template> + </template> + </studip-dialog> +</template> +<script> +import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; +import Datepicker from './../../Datepicker.vue'; +import axios from 'axios'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-unit-item-dialog-permissions', + components: { + CoursewareCompanionBox, + Datepicker, + }, + props: { + unit: { + type: Object, + required: true, + }, + unitName: { + type: String, + required: true + } + }, + data() { + return { + permissionType: 'all', + visible: 'always', + visibleAll: false, + visibleApprovalUsers: [], + visibleApprovalGroups: [], + visibleStartDate: null, + visibleEndDate: null, + writable: 'never', + writableAll: false, + writableStartDate: null, + writableEndDate: null, + writableApprovalUsers: [], + writableApprovalGroups: [], + height: '680', + width: '870', + currentSemester: null, + }; + }, + computed: { + ...mapGetters({ + context: 'context', + relatedCourseMemberships: 'course-memberships/related', + relatedCourseStatusGroups: 'status-groups/related', + relatedUser: 'users/related', + }), + users() { + const parent = { type: 'courses', id: this.context.id }; + const relationship = 'memberships'; + const memberships = this.relatedCourseMemberships({ parent, relationship }); + + return ( + memberships?.map((membership) => { + const parent = { type: membership.type, id: membership.id }; + const member = this.relatedUser({ parent, relationship: 'user' }); + + return { + id: member.id, + formattedname: member.attributes['formatted-name'], + username: member.attributes['username'], + perm: membership.attributes['permission'], + }; + }) ?? [] + ); + }, + statusGroupsUrl() { + return STUDIP.URLHelper.getURL('dispatch.php/course/statusgroups'); + }, + autorMembers() { + if (Object.keys(this.users).length === 0 && this.users.constructor === Object) { + return []; + } + + let members = this.users.filter(function (user) { + return user.perm === 'autor'; + }); + + return members; + }, + groups() { + const parent = { type: 'courses', id: this.context.id }; + const relationship = 'status-groups'; + const statusGroups = this.relatedCourseStatusGroups({ parent, relationship }); + + return ( + statusGroups?.map((statusGroup) => { + return { + id: statusGroup.id, + name: statusGroup.attributes['name'], + }; + }) ?? [] + ); + }, + periodsValid() { + if (this.writable !== 'period' || this.visible !== 'period') { + return true; + } + return this.visibleStartDate <= this.writableStartDate + && ( + this.visibleEndDate === null + || this.visibleEndDate >= this.writableEndDate + ); + }, + semesterDates() { + const date = Date.now() / 1000; + let startDate = date; + let endDate = date; + if (this.currentSemester) { + startDate = new Date(this.currentSemester.attributes.start).getTime() / 1000; + endDate = new Date(this.currentSemester.attributes.end).getTime() / 1000; + } + + return { start: startDate, end: endDate }; + }, + dialogTitle() { + return this.$gettext('Rechte und Sichtbarkeit') + ': ' + this.unitName; + } + }, + methods: { + ...mapActions({ + loadCourseMemberships: 'course-memberships/loadRelated', + loadCourseStatusGroups: 'status-groups/loadRelated', + updateUnit: 'courseware-units/update', + loadUnit: 'courseware-units/loadById', + companionWarning: 'companionWarning', + }), + setDimensions() { + this.height = Math.min((window.innerHeight * 0.8).toFixed(0), 680).toString(); + this.width = Math.min(window.innerWidth * 0.8, 870).toFixed(0); + }, + initData() { + this.permissionType = this.unit.attributes['permission-type']; + this.visible = this.unit.attributes['visible']; + this.visibleAll = this.unit.attributes['visible-all']; + this.visibleStartDate = this.unit.attributes['visible-start-date'] + ? new Date(this.unit.attributes['visible-start-date']).getTime() / 1000 + : null; + this.visibleEndDate = this.unit.attributes['visible-end-date'] + ? new Date(this.unit.attributes['visible-end-date']).getTime() / 1000 + : null; + this.writable = this.unit.attributes['writable']; + this.writableAll = this.unit.attributes['writable-all']; + this.writableStartDate = this.unit.attributes['writable-start-date'] + ? new Date(this.unit.attributes['writable-start-date']).getTime() / 1000 + : null; + this.writableEndDate = this.unit.attributes['writable-end-date'] + ? new Date(this.unit.attributes['writable-end-date']).getTime() / 1000 + : null; + if (this.permissionType === 'users') { + this.visibleApprovalUsers = this.unit.attributes['visible-approval']; + this.writableApprovalUsers = this.unit.attributes['writable-approval']; + } + if (this.permissionType === 'groups') { + this.visibleApprovalGroups = this.unit.attributes['visible-approval']; + this.writableApprovalGroups = this.unit.attributes['writable-approval']; + } + + axios + .get(STUDIP.URLHelper.getURL('jsonapi.php/v1/semesters', { 'filter[current]': true }, true)) + .then((response) => { + this.currentSemester = response.data.data[0]; + }) + .catch((error) => { + this.currentSemester = null; + }); + }, + async storePermissions() { + let visibleApproval = []; + let writableApproval = []; + if (this.permissionType === 'users') { + visibleApproval = this.visibleApprovalUsers; + writableApproval = this.writableApprovalUsers; + } + if (this.permissionType === 'groups') { + visibleApproval = this.visibleApprovalGroups; + writableApproval = this.writableApprovalGroups; + } + + if (this.visible === 'period' && this.visibleStartDate === null && this.visibleEndDate === null) { + this.visible = 'always'; + } + + if (this.writable === 'period' && this.writableStartDate === null && this.writableEndDate === null) { + this.visible = 'always'; + } + + if ( + this.visible === 'period' && + this.visibleStartDate !== null && + this.visibleEndDate !== null && + this.visibleStartDate > this.visibleEndDate + ) { + this.companionWarning({ + info: this.$gettext( + 'Das Enddatum des Sichtbarkeitszeitraums darf nicht vor dem Startdatum liegen.' + ), + }); + return false; + } + + if ( + this.writable === 'period' && + this.writableStartDate !== null && + this.writableEndDate !== null && + this.writableStartDate > this.writableEndDate + ) { + this.companionWarning({ + info: this.$gettext('Das Enddatum des Bearbeitungszeitraums darf nicht vor dem Startdatum liegen.'), + }); + return false; + } + + if (!this.periodsValid) { + this.companionWarning({ + info: this.$gettext('Der Bearbeitungszeitraum muss innerhalb des Sichtbarkeitszeitraums liegen.'), + }); + return false; + } + + const unit = { + id: this.unit.id, + type: 'courseware-units', + attributes: { + 'permission-scope': 'unit', + 'permission-type': this.permissionType, + visible: this.visible, + 'visible-all': this.visibleAll && this.permissionType !== 'all' ? 1 : 0, + 'visible-start-date': + this.visible === 'period' ? new Date(this.visibleStartDate * 1000).toISOString() : null, + 'visible-end-date': + this.visible === 'period' ? new Date(this.visibleEndDate * 1000).toISOString() : null, + 'visible-approval': JSON.stringify(visibleApproval), + writable: this.writable, + 'writable-all': this.writableAll && this.permissionType !== 'all' ? 1 : 0, + 'writable-start-date': + this.writable === 'period' ? new Date(this.writableStartDate * 1000).toISOString() : null, + 'writable-end-date': + this.writable === 'period' ? new Date(this.writableEndDate * 1000).toISOString() : null, + 'writable-approval': JSON.stringify(writableApproval), + }, + }; + this.$emit('close'); + await this.updateUnit(unit); + await this.loadUnit({ id: this.unit.id }); + }, + + updatePermissionType() { + if (this.permissionType !== 'all') { + if (this.visible === 'never') { + this.visible = 'always'; + } + if (this.writable === 'never') { + this.writable = 'always'; + } + } else { + if (this.writable === 'always') { + this.writable = 'never'; + } + } + }, + updateVisibile() { + if (this.visible === 'never' && this.permissionType === 'all') { + this.writable = 'never'; + } + if (this.visible === 'period') { + if (this.writable === 'always') { + this.writable = 'period'; + this.writableStartDate = this.writableStartDate ?? this.semesterDates.start; + this.writableEndDate = this.writableEndDate ?? this.semesterDates.end; + } + + this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start; + this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end; + } + }, + updateWritable() { + if (this.writable === 'always') { + this.visible = 'always'; + } + if (this.writable === 'period' && this.permissionType === 'all' && this.visible !== 'always') { + this.visible = 'period'; + this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start; + this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end; + } + if (this.writable === 'period') { + this.writableStartDate = this.writableStartDate ?? this.semesterDates.start; + this.writableEndDate = this.writableEndDate ?? this.semesterDates.end; + } + }, + updateUserWritable(user) { + if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) { + this.visibleApprovalUsers.push(user.id); + } + }, + updateUserVisible(user) { + if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) { + this.writableApprovalUsers = this.writableApprovalUsers.filter((id) => id !== user.id); + } + }, + updateGroupWritable(group) { + if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) { + this.visibleApprovalGroups.push(group.id); + } + }, + updateGroupVisible(group) { + if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) { + this.writableApprovalGroups = this.writableApprovalGroups.filter((id) => id !== group.id); + } + }, + updateVisibleAll() { + if (this.writableAll) { + this.visibleAll = true; + } + }, + updatewritableAll() { + if (!this.visibleAll) { + this.writableAll = false; + } + }, + }, + mounted() { + this.setDimensions(); + this.initData(); + const parent = { type: 'courses', id: this.context.id }; + let options = { + include: 'user', + 'page[limit]': 10000, + }; + this.loadCourseMemberships({ parent, relationship: 'memberships', options: options }); + this.loadCourseStatusGroups({ parent, relationship: 'status-groups' }); + }, +}; +</script> diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItems.vue b/resources/vue/components/courseware/unit/CoursewareUnitItems.vue index e805e1104bc76b88ff3738ff4b304ff8e3285213..8f747bfc445fbb07382dbbb1129a467362984698 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitItems.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitItems.vue @@ -3,7 +3,9 @@ <h2 v-if="!inCourseContext && hasUnits">{{ $gettext('Persönliche Lernmaterialien') }}</h2> <template v-if="hasUnits"> <ol v-if="(!userIsTeacher && inCourseContext) || units.length === 1" class="cw-tiles"> - <courseware-unit-item v-for="unit in units" :key="unit.id" :unit="unit" :handle="false"/> + <template v-for="unit in units"> + <courseware-unit-item :key="unit.id" :unit="unit" :handle="false"/> + </template> </ol> <template v-else> <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span> diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js index 76f8034b675df7940155ec50e93c19e65131191b..a82dc9888a210b2cd29c9a837377253cebf203ff 100644 --- a/resources/vue/courseware-index-app.js +++ b/resources/vue/courseware-index-app.js @@ -152,7 +152,7 @@ const mountApp = async (STUDIP, createApp, element) => { type: entry_type, unit: unit_id }); - + store.dispatch('courseware-units/loadById', { id: unit_id }); if (entry_type === 'courses') { store.dispatch('loadProgresses'); await store.dispatch('setFeedbackSettings', feedbackSettings); diff --git a/resources/vue/courseware-shelf-app.js b/resources/vue/courseware-shelf-app.js index 82ece71b0e462938f42741a6665f9cf521d405e3..35369c1c6d3c5b8ecc8b5f0cfc6791c33caedc71 100644 --- a/resources/vue/courseware-shelf-app.js +++ b/resources/vue/courseware-shelf-app.js @@ -84,6 +84,7 @@ const mountApp = async (STUDIP, createApp, element) => { 'sem-classes', 'sem-types', 'stock-images', + 'status-groups', 'terms-of-use' ], httpClient, diff --git a/resources/vue/store/courseware/courseware-shelf.module.js b/resources/vue/store/courseware/courseware-shelf.module.js index e5ea54c48371760a0cfbbc1f8a91acc0e1ad9062..04d68d1118ad2e453f0a1033abcb655bf6ccf03c 100644 --- a/resources/vue/store/courseware/courseware-shelf.module.js +++ b/resources/vue/store/courseware/courseware-shelf.module.js @@ -283,7 +283,7 @@ export const actions = { } }, - async copyUnit({ dispatch, state }, { unitId, modified }) { + async copyUnit({ dispatch, state }, { unitId, modified, duplicate }) { let rangeType = null; let loadUnits = null; if (state.context.type === 'courses') { @@ -297,7 +297,7 @@ export const actions = { if(!rangeType) { return false; } - const copy = { data: { rangeId: state.context.id, rangeType: rangeType, modified: modified } }; + const copy = { data: { rangeId: state.context.id, rangeType: rangeType, modified: modified, duplicate: duplicate} }; await state.httpClient.post(`courseware-units/${unitId}/copy`, copy); return dispatch(loadUnits, state.context.id); diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index b6a383b7ed9d1cc054bf4f7349566f94effc1615..53ef6aa396de1f19c953746a8dc6c33513be1ee1 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -39,6 +39,7 @@ const getDefaultState = () => { showStructuralElementRemoveLockDialog: false, showStructuralElementFeedbackDialog: false, showStructuralElementFeedbackCreateDialog: false, + showStructuralElementPermissionsDialog: false, showSuggestOerDialog: false, @@ -251,6 +252,9 @@ const getters = { showStructuralElementFeedbackCreateDialog(state) { return state.showStructuralElementFeedbackCreateDialog; }, + showStructuralElementPermissionsDialog(state) { + return state.showStructuralElementPermissionsDialog; + }, showOverviewElementAddDialog(state) { return state.showOverviewElementAddDialog; }, @@ -1041,7 +1045,7 @@ export const actions = { context.commit('setShowStructuralElementInfoDialog', bool); }, - showElementOerDialog(context, bool) { + showElementOerExportDialog(context, bool) { context.commit('setShowStructuralElementOerDialog', bool); }, @@ -1067,6 +1071,9 @@ export const actions = { showStructuralElementFeedbackCreateDialog(context, bool) { context.commit('setShowStructuralElementFeedbackCreateDialog', bool); }, + showStructuralElementPermissionsDialog(context, bool) { + context.commit('setShowStructuralElementPermissionsDialog', bool); + }, setShowOverviewElementAddDialog(context, bool) { context.commit('setShowOverviewElementAddDialog', bool); @@ -1664,6 +1671,9 @@ export const mutations = { setShowStructuralElementFeedbackCreateDialog(state, showFeedbackCreate) { state.showStructuralElementFeedbackCreateDialog = showFeedbackCreate; }, + setShowStructuralElementPermissionsDialog(state, showPermissionsDialog) { + state.showStructuralElementPermissionsDialog = showPermissionsDialog; + }, setImportFilesState(state, importFilesState) { state.importFilesState = importFilesState;