From c7c10f40175d34d9c76fa90d328ff0cdc2c7dfd2 Mon Sep 17 00:00:00 2001 From: Ron Lucke <lucke@elan-ev.de> Date: Mon, 4 Nov 2024 11:39:35 +0000 Subject: [PATCH] Courseware: Rechte und Sichtbarkeit funktionieren nach unterschiedlichen Kriterien Closes #3442 Merge request studip/studip!2635 --- db/migrations/6.0.27_add_unit_permissions.php | 93 ++ ....28_add_structural_element_permissions.php | 100 ++ ..._update_structural_element_permissions.php | 75 ++ .../Routes/Courseware/CoursesUnitsIndex.php | 10 +- .../Courseware/StructuralElementsCreate.php | 14 +- .../StructuralElementsReleasedIndex.php | 2 +- .../StructuralElementsSharedIndex.php | 10 +- .../Courseware/StructuralElementsUpdate.php | 59 +- .../JsonApi/Routes/Courseware/UnitsCopy.php | 3 +- .../JsonApi/Routes/Courseware/UnitsCreate.php | 12 +- .../JsonApi/Routes/Courseware/UnitsUpdate.php | 95 +- lib/classes/JsonApi/Routes/SemestersIndex.php | 23 +- .../Schemas/Courseware/StructuralElement.php | 16 +- .../JsonApi/Schemas/Courseware/Unit.php | 23 +- lib/models/Courseware/StructuralElement.php | 329 +++--- lib/models/Courseware/Unit.php | 161 ++- .../assets/stylesheets/scss/courseware.scss | 2 + .../containers/default-container.scss | 64 - .../scss/courseware/layouts/permissions.scss | 32 + .../scss/courseware/layouts/radioset.scss | 88 ++ .../scss/courseware/layouts/tile.scss | 37 +- resources/assets/stylesheets/scss/dialog.scss | 2 +- .../CoursewareContentPermissions.vue | 113 +- .../courseware/CoursewareContentShared.vue | 10 +- .../containers/CoursewareDefaultContainer.vue | 6 +- .../courseware/layouts/CoursewareTile.vue | 13 +- .../CoursewareRootContent.vue | 9 +- .../CoursewareStructuralElement.vue | 1043 +++++------------ ...reStructuralElementDialogExportChooser.vue | 4 +- ...sewareStructuralElementDialogExportOer.vue | 110 ++ ...sewareStructuralElementDialogExportPdf.vue | 6 +- .../CoursewareStructuralElementDialogInfo.vue | 72 ++ ...ewareStructuralElementDialogOerSuggest.vue | 72 ++ ...wareStructuralElementDialogPermissions.vue | 619 ++++++++++ ...ewareStructuralElementDialogPublicLink.vue | 84 ++ ...rsewareStructuralElementDialogSettings.vue | 342 ++++++ .../structural-element/CoursewareTree.vue | 7 +- .../structural-element/CoursewareTreeItem.vue | 163 ++- .../unit/CoursewareShelfDialogCopy.vue | 3 +- .../courseware/unit/CoursewareUnitItem.vue | 338 +++++- ...oursewareUnitItemDialogPermissionScope.vue | 44 + .../CoursewareUnitItemDialogPermissions.vue | 585 +++++++++ .../courseware/unit/CoursewareUnitItems.vue | 4 +- resources/vue/courseware-index-app.js | 2 +- resources/vue/courseware-shelf-app.js | 1 + .../courseware/courseware-shelf.module.js | 4 +- .../vue/store/courseware/courseware.module.js | 12 +- 47 files changed, 3700 insertions(+), 1216 deletions(-) create mode 100644 db/migrations/6.0.27_add_unit_permissions.php create mode 100644 db/migrations/6.0.28_add_structural_element_permissions.php create mode 100644 db/migrations/6.0.29_update_structural_element_permissions.php create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/permissions.scss create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/radioset.scss create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportOer.vue create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogInfo.vue create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogOerSuggest.vue create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogSettings.vue create mode 100644 resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissionScope.vue create mode 100644 resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue 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 00000000000..c275a58561f --- /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 00000000000..8c5b5dd9209 --- /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 00000000000..5de86305ea5 --- /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 b09d0c8b5c1..9a3425f1cb4 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 c038c45f847..99481f57fd8 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 b4a8e1c1108..699623b03a2 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 1582fbfb18d..9ebf923d450 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 455aacc3c06..3b3deb0a920 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 61ffa956cf2..62a1d0a81e2 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 9dfd2e63c04..8f3e217e8f3 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 446d61e2872..4c48086ae05 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 ffa1d4c0f7b..24883c19f3d 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 e6ccafa2f88..f1a584137c6 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 6152e940e61..a311ac8286b 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 8e06a396af9..6f87e70d94d 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 2a38a291d40..86013e368db 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 c555b6dbeee..8c9d87f64cc 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 baed5032963..948339ce946 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 00000000000..fa84a1f4a14 --- /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 00000000000..a65a189b3a1 --- /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 a5632442a33..7b56857b0d4 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 f45dc9b4e0c..bd8ad030e68 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 60f09644471..841fc8ffd8a 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 8eb9bf6104a..0bdc4b5cfd2 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 427a93092c6..7f4a882e84f 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 1b37511eb3d..d2f53c35356 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 1aa88028ac2..ee820c4a175 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 d423eacf695..2b7cdbc610b 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 be5dd8d52dd..ec072900aa5 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 00000000000..8059ebfb4cd --- /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 a2aca9d42f2..7cb88d9bf70 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 00000000000..fcc5a8796be --- /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 00000000000..30a57d44128 --- /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 00000000000..930cd909ae5 --- /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 00000000000..37a32228f5c --- /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 00000000000..dd81d541a86 --- /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 08019dd26d7..bd25c6b6690 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 540e76b3529..6fe7da87d39 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 e0299968a62..c7447cea585 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 dc68225f873..f2c4d4a73f5 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 00000000000..46a85c5c2cd --- /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 00000000000..86ac6412852 --- /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 e805e1104bc..8f747bfc445 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 76f8034b675..a82dc9888a2 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 82ece71b0e4..35369c1c6d3 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 e5ea54c4837..04d68d1118a 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 b6a383b7ed9..53ef6aa396d 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; -- GitLab