From ec684bbd1629803bee4c15faf78c9997aaf7daf5 Mon Sep 17 00:00:00 2001 From: Ron Lucke Date: Mon, 26 Sep 2022 08:11:22 +0000 Subject: =?UTF-8?q?StEP00362:=20Rechte-=20und=20Zugriffsverwaltung=20f?= =?UTF-8?q?=C3=BCr=20Arbeitsplatz=20>=20Lernmaterialien?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #919 Merge request studip/studip!639 --- app/controllers/contents/courseware.php | 37 ++ app/controllers/multipersonsearch.php | 40 +- .../courseware/shared_content_courseware.php | 10 + lib/classes/JsonApi/RouteMap.php | 6 +- .../JsonApi/Routes/Courseware/Authority.php | 61 ++- .../Courseware/CoursewareInstancesHelper.php | 1 + .../Courseware/StructuralElementsReleasedIndex.php | 59 +++ .../Courseware/StructuralElementsSharedIndex.php | 77 ++++ lib/models/Courseware/StructuralElement.php | 166 +++++-- lib/navigation/ContentsNavigation.php | 54 ++- .../assets/javascripts/bootstrap/courseware.js | 2 +- resources/assets/stylesheets/scss/courseware.scss | 73 ++- .../stylesheets/scss/multi_person_search.scss | 8 + resources/assets/stylesheets/studip.scss | 1 + resources/vue/base-components.js | 4 +- resources/vue/components/StudipIcon.vue | 3 +- .../vue/components/StudipMultiPersonSearch.vue | 190 ++++++++ .../components/courseware/ContentOverviewApp.vue | 4 +- .../components/courseware/ContentReleasesApp.vue | 6 +- .../CoursewareContentOverviewElements.vue | 504 ++++++++++++--------- .../CoursewareContentOverviewFilterWidget.vue | 106 +++-- .../courseware/CoursewareContentPermissions.vue | 391 ++++++++++++++++ .../courseware/CoursewareContentShared.vue | 173 +++++++ .../courseware/CoursewareStructuralElement.vue | 24 +- .../components/courseware/CoursewareTreeItem.vue | 3 + resources/vue/courseware-content-overview-app.js | 3 + resources/vue/courseware-content-releases-app.js | 5 +- .../vue/store/courseware/courseware.module.js | 20 + 28 files changed, 1704 insertions(+), 327 deletions(-) create mode 100755 app/views/contents/courseware/shared_content_courseware.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php create mode 100755 resources/assets/stylesheets/scss/multi_person_search.scss create mode 100644 resources/vue/components/StudipMultiPersonSearch.vue create mode 100755 resources/vue/components/courseware/CoursewareContentPermissions.vue create mode 100644 resources/vue/components/courseware/CoursewareContentShared.vue diff --git a/app/controllers/contents/courseware.php b/app/controllers/contents/courseware.php index c7edcac..860fdc6 100644 --- a/app/controllers/contents/courseware.php +++ b/app/controllers/contents/courseware.php @@ -432,4 +432,41 @@ class Contents_CoursewareController extends AuthenticatedController $this->render_pdf($element->pdfExport($this->user, $with_children), trim($element->title).'.pdf'); } + + /** + * To display the shared courseware + * + * @param string $entry_element_id the shared struct element id + */ + public function shared_content_courseware_action($entry_element_id) + { + global $perm, $user; + + $navigation = new Navigation(_('Geteiltes Lernmaterial'), 'dispatch.php/contents/courseware/shared_content_courseware/' . $entry_element_id); + Navigation::addItem('/contents/courseware/shared_content_courseware', $navigation); + Navigation::activateItem('/contents/courseware/shared_content_courseware'); + + $this->entry_element_id = $entry_element_id; + + $struct = \Courseware\StructuralElement::findOneBySQL( + "id = ? AND range_type = 'user'", + [$this->entry_element_id] + ); + + if (!$struct) { + throw new Trails_Exception(404, _('Der geteilte Inhalt kann nicht gefunden werden.')); + } + + if (!$struct->canRead($user) && !$struct->canEdit($user)) { + throw new AccessDeniedException(); + } + + $this->user_id = $struct->owner_id; + + $this->licenses = $this->getLicences(); + + $this->oer_enabled = Config::get()->OERCAMPUS_ENABLED && $perm->have_perm(Config::get()->OER_PUBLIC_STATUS); + + $this->setCoursewareSidebar(); + } } diff --git a/app/controllers/multipersonsearch.php b/app/controllers/multipersonsearch.php index d036315..01733cb 100644 --- a/app/controllers/multipersonsearch.php +++ b/app/controllers/multipersonsearch.php @@ -110,7 +110,8 @@ class MultipersonsearchController extends AuthenticatedController * This needs to be done in one single action to provider a similar * usability for no-JavaScript users as for JavaScript users. */ - public function no_js_form_action() { + public function no_js_form_action() + { if (!empty($_POST)) { CSRFProtection::verifyUnsafeRequest(); @@ -243,4 +244,41 @@ class MultipersonsearchController extends AuthenticatedController } + + public function ajax_search_vue_action($name) + { + $searchterm = Request::get('s'); + $searchterm = str_replace(',', ' ', $searchterm); + $searchterm = preg_replace('/\s+/u', ' ', $searchterm); + + $result = []; + // execute searchobject if searchterm is at least 3 chars long + if (mb_strlen($searchterm) >= 3) { + $mp = MultiPersonSearch::load($name); + $mp->setSearchObject(new StandardSearch('user_id')); + $searchObject = $mp->getSearchObject(); + $result = array_map(function ($r) { + return $r['user_id']; + }, $searchObject->getResults($searchterm, [], 50)); + $result = User::findFullMany($result, 'ORDER BY Nachname ASC, Vorname ASC'); + $alreadyMember = $mp->getDefaultSelectedUsersIDs(); + } + + $output = []; + foreach ($result as $user) { + $output[] = [ + 'id' => $user->id, + 'avatar' => Avatar::getAvatar($user->id)->getURL(Avatar::SMALL), + 'text' => "{$user->nachname}, {$user->vorname} -- {$user->perms} ({$user->username})", + 'selected' => $alreadyMember === null ? false : in_array($user->id, $alreadyMember), + 'nachname' => $user->nachname, + 'vorname' => $user->vorname, + 'username' => $user->username, + 'formatted-name' => trim($user->getFullName()) + ]; + } + $this->render_json($output); + } + + } diff --git a/app/views/contents/courseware/shared_content_courseware.php b/app/views/contents/courseware/shared_content_courseware.php new file mode 100755 index 0000000..f959059 --- /dev/null +++ b/app/views/contents/courseware/shared_content_courseware.php @@ -0,0 +1,10 @@ +
+
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 478e319..312a90a 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -308,7 +308,7 @@ class RouteMap private function addAuthenticatedCoursewareRoutes(RouteCollectorProxy $group): void { - $group->get('/{type:courses|users}/{id}/courseware', Routes\Courseware\CoursewareInstancesShow::class); + $group->get('/{type:courses|users|sharedusers}/{id}/courseware', Routes\Courseware\CoursewareInstancesShow::class); $group->patch('/courseware-instances/{id}', Routes\Courseware\CoursewareInstancesUpdate::class); $this->addRelationship( $group, @@ -420,6 +420,10 @@ class RouteMap $group->patch('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackUpdate::class); $group->delete('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackDelete::class); + $group->get('/courseware-structural-elements-shared', Routes\Courseware\StructuralElementsSharedIndex::class); + $group->get('/courseware-structural-elements-released', Routes\Courseware\StructuralElementsReleasedIndex::class); + + $group->get('/courseware-blocks/{id}/user-data-field', Routes\Courseware\UserDataFieldOfBlocksShow::class); $group->get('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsShow::class); $group->patch('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsUpdate::class); diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php index 5e30a41..29bde20 100644 --- a/lib/classes/JsonApi/Routes/Courseware/Authority.php +++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php @@ -59,7 +59,23 @@ class Authority public static function canUpdateBlock(User $user, Block $resource) { if ($resource->isBlocked()) { - return $resource->getBlockerUserId() == $user->id; + $structural_element = $resource->container->structural_element; + + if ($structural_element->range_type === 'user') { + if ($structural_element->range_id === $user->id) { + return true; + } + + return $structural_element->canEdit($user); + } + + $perm = $GLOBALS['perm']->have_studip_perm( + $structural_element->course->config->COURSEWARE_EDITING_PERMISSION, + $structural_element->course->id, + $user->id + ); + + return $resource->getBlockerUserId() === $user->id || $perm; } return self::canUpdateContainer($user, $resource->container); @@ -72,7 +88,36 @@ class Authority public static function canUpdateEditBlocker(User $user, $resource) { - return $resource->edit_blocker_id == '' || $resource->edit_blocker_id === $user->id; + $structural_element = null; + if ($resource instanceof Block) { + $structural_element = $resource->container->structural_element; + } + if ($resource instanceof Container) { + $structural_element = $resource->structural_element; + } + if ($resource instanceof StructuralElement) { + $structural_element = $resource; + } + + if ($structural_element === null) { + return false; + } + + if ($structural_element->range_type === 'user') { + if ($structural_element->range_id === $user->id) { + return true; + } + + return $structural_element->canEdit($user); + } + + $perm = $GLOBALS['perm']->have_studip_perm( + $structural_element->course->config->COURSEWARE_EDITING_PERMISSION, + $structural_element->course->id, + $user->id + ); + + return $resource->edit_blocker_id == '' || $resource->edit_blocker_id === $user->id || $perm; } public static function canShowContainer(User $user, Container $resource) @@ -163,6 +208,18 @@ class Authority return $GLOBALS['perm']->have_perm('root', $user->id); } + public static function canIndexStructuralElementsShared(User $user) + { + //TODO ? + return true; + } + + public static function canIndexStructuralElementsReleased(User $user) + { + //TODO ? + return true; + } + public static function canReorderStructuralElements(User $user, $resource) { return self::canUpdateStructuralElement($user, $resource); diff --git a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php index 843f7c2..46a2e68 100644 --- a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php +++ b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php @@ -26,6 +26,7 @@ trait CoursewareInstancesHelper 'courses' => 'getCoursewareCourse', 'user' => 'getCoursewareUser', 'users' => 'getCoursewareUser', + 'sharedusers' => 'getSharedCoursewareUser', ]; if (!($method = $methods[$rangeType])) { throw new BadRequestException('Invalid range type: "' . $rangeType . '".'); diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php new file mode 100644 index 0000000..b4a8e1c --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php @@ -0,0 +1,59 @@ +getUser($request); + if (!Authority::canIndexStructuralElementsReleased($user)) { + throw new AuthorizationFailedException(); + } + + list($offset, $limit) = $this->getOffsetAndLimit(); + $resources = []; + $contents = StructuralElement::findBySQL( + 'range_id = ? AND range_type = ? ORDER BY mkdate DESC', + [$user->id, 'user'] + ); + + 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)) { + $resources[] = $content; + } + } + + return $this->getPaginatedContentResponse($resources, count($resources)); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php new file mode 100644 index 0000000..0dc7c0d --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php @@ -0,0 +1,77 @@ +getUser($request); + if (!Authority::canIndexStructuralElementsShared($user)) { + throw new AuthorizationFailedException(); + } + + list($offset, $limit) = $this->getOffsetAndLimit(); + $resources = []; + $contents = StructuralElement::findBySQL( + 'range_id != ? AND range_type = ? ORDER BY mkdate DESC', + [$user->id, 'user'] + ); + + foreach ($contents as $content) { + if (!count($content->read_approval) || !count($content->write_approval)) { + 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) { + if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read']) { + $add_content = true; + } + } + + if ($add_content) { + $resources[] = $content; + } + } + + return $this->getPaginatedContentResponse($resources, count($resources)); + } +} diff --git a/lib/models/Courseware/StructuralElement.php b/lib/models/Courseware/StructuralElement.php index 8edabad..c242870 100644 --- a/lib/models/Courseware/StructuralElement.php +++ b/lib/models/Courseware/StructuralElement.php @@ -169,14 +169,22 @@ class StructuralElement extends \SimpleORMap return self::getCourseware($courseId, 'course'); } - private static function getCourseware(string $rangeId, string $rangeType): ?StructuralElement + public static function getSharedCoursewareUser(string $root_id): ?StructuralElement { - /** @var ?StructuralElement $result */ - $result = self::findOneBySQL( - 'range_id = ? - AND range_type = ? AND parent_id IS NULL', - [$rangeId, $rangeType] - ); + return self::getCourseware('', '', $root_id); + } + + private static function getCourseware(string $rangeId, string $rangeType, string $root_id = null): ?StructuralElement + { + if ($root_id) { + $result = self::find($root_id); + } else { + $result = self::findOneBySQL( + 'range_id = ? + AND range_type = ? AND parent_id IS NULL', + [$rangeId, $rangeType] + ); + } return $result; } @@ -222,7 +230,11 @@ class StructuralElement extends \SimpleORMap switch ($this->range_type) { case 'user': - return $this->range_id === $user->id; + if ($this->range_id === $user->id) { + return true; + } + + return $this->hasWriteApproval($user); case 'course': $hasEditingPermission = $this->hasEditingPermission($user); @@ -273,11 +285,12 @@ class StructuralElement extends \SimpleORMap switch ($this->range_type) { case 'user': - // Kontext "user": Nutzende können nur ihre eigenen Strukturknoten sehen. - if ($this->range_id === $user->id) { + if ($this->range_id === $user->id) { return true; } + return $this->hasReadApproval($user); + $link = StructuralElement::findOneBySQL('target_id = ?', [$this->id]); if ($link) { return true; @@ -313,8 +326,11 @@ class StructuralElement extends \SimpleORMap switch ($this->range_type) { case 'user': - // Kontext "user": Nutzende können nur ihre eigenen Strukturknoten sehen. - return $this->range_id === $user->id; + if ($this->range_id === $user->id) { + return true; + } + + return $this->hasReadApproval($user); case 'course': if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) { @@ -367,59 +383,133 @@ class StructuralElement extends \SimpleORMap private function hasReadApproval($user): bool { - if (!count($this->read_approval)) { + // this property is shared between all range types. + if ($this->read_approval['all']) { return true; } - 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)) { + 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']; + foreach ($groups as $groupId) { + /** @var ?\Statusgruppen $group */ + $group = \Statusgruppen::find($groupId); + if ($group && $group->isMember($user->id)) { + return true; + } + } + } + + return false; + } + + private function hasUserReadApproval($user): bool + { + if (!count($this->read_approval)) { + if ($this->isRootNode()) { + return false; + } + return $this->parent->hasUserReadApproval($user); } // find user in users $users = $this->read_approval['users']; - foreach ($users as $approvedUserId) { - if ($approvedUserId == $user->id) { + 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); + } + // 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; } } + } + + private function hasWriteApproval($user): bool + { + // this property is shared between all range types. + if ($this->write_approval['all']) { + return true; + } - // find user in groups - $groups = $this->read_approval['groups']; - foreach ($groups as $groupId) { - /** @var ?\Statusgruppen $group */ - $group = \Statusgruppen::find($groupId); - if ($group && $group->isMember($user->id)) { + // 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)) { return true; } + + // find user in groups + foreach (\Statusgruppen::findMany($this->write_approval['groups']->getArrayCopy()) as $group) { + if ($group->isMember($user->id)) { + return true; + } + } } return false; } - private function hasWriteApproval($user): bool + private function hasUserWriteApproval($user): bool { if (!count($this->write_approval)) { - return false; - } - - if ($this->write_approval['all']) { - return true; + if ($this->isRootNode()) { + return false; + } + return $this->parent->hasUserWriteApproval($user); } // find user in users - $users = $this->write_approval['users']->getArrayCopy(); - if (in_array($user->id, $users)) { - return true; - } - - // find user in groups - foreach (\Statusgruppen::findMany($this->write_approval['groups']->getArrayCopy()) as $group) { - if ($group->isMember($user->id)) { + $users = $this->write_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); + } + if ($listedUserPerm['id'] == $user->id) { return true; } } - return false; + if ($this->isRootNode()) { + return false; + } + return $this->parent->hasUserWriteApproval($user); } /** diff --git a/lib/navigation/ContentsNavigation.php b/lib/navigation/ContentsNavigation.php index 32cefa5..8e814ef 100644 --- a/lib/navigation/ContentsNavigation.php +++ b/lib/navigation/ContentsNavigation.php @@ -47,30 +47,36 @@ class ContentsNavigation extends Navigation $courseware->setDescription(_('Erstellen und Sammeln von Lernmaterialien')); $courseware->setImage(Icon::create('courseware')); - $courseware->addSubNavigation( - 'overview', - new Navigation(_('Übersicht'), 'dispatch.php/contents/courseware/index') - ); - $courseware->addSubNavigation( - 'courseware', - new Navigation(_('Persönliche Lernmaterialien'), 'dispatch.php/contents/courseware/courseware') - ); - $courseware->addSubNavigation( - 'courseware_manager', - new Navigation(_('Verwaltung persönlicher Lernmaterialien'), 'dispatch.php/contents/courseware/courseware_manager') - ); - $courseware->addSubNavigation( - 'releases', - new Navigation(_('Freigaben'), 'dispatch.php/contents/courseware/releases') - ); - $courseware->addSubNavigation( - 'bookmarks', - new Navigation(_('Lesezeichen'), 'dispatch.php/contents/courseware/bookmarks') - ); - $courseware->addSubNavigation( - 'courses_overview', - new Navigation(_('Meine Veranstaltungen'), 'dispatch.php/contents/courseware/courses_overview') - ); + $courseware = new Navigation(_('Courseware')); + $courseware->setDescription(_('Erstellen und Sammeln von Lernmaterialien')); + $courseware->setImage(Icon::create('courseware')); + + $courseware->addSubNavigation( + 'overview', + new Navigation(_('Übersicht'), 'dispatch.php/contents/courseware/index') + ); + $courseware->addSubNavigation( + 'courseware', + new Navigation(_('Persönliche Lernmaterialien'), 'dispatch.php/contents/courseware/courseware') + ); + $courseware->addSubNavigation( + 'courseware_manager', + new Navigation(_('Verwaltung persönlicher Lernmaterialien'), 'dispatch.php/contents/courseware/courseware_manager') + ); + $courseware->addSubNavigation( + 'releases', + new Navigation(_('Freigaben'), 'dispatch.php/contents/courseware/releases') + ); + $courseware->addSubNavigation( + 'bookmarks', + new Navigation(_('Lesezeichen'), 'dispatch.php/contents/courseware/bookmarks') + ); + $courseware->addSubNavigation( + 'courses_overview', + new Navigation(_('Meine Veranstaltungen'), 'dispatch.php/contents/courseware/courses_overview') + ); + + $this->addSubNavigation('courseware', $courseware); $this->addSubNavigation('courseware', $courseware); } diff --git a/resources/assets/javascripts/bootstrap/courseware.js b/resources/assets/javascripts/bootstrap/courseware.js index 8710150..7124ac9 100644 --- a/resources/assets/javascripts/bootstrap/courseware.js +++ b/resources/assets/javascripts/bootstrap/courseware.js @@ -79,7 +79,7 @@ STUDIP.domReady(() => { if (document.getElementById('courseware-content-releases-app')) { STUDIP.Vue.load().then(({ createApp }) => { import( - /* webpackChunkName: "courseware-content-links-app" */ + /* webpackChunkName: "courseware-content-releases-app" */ '@/vue/courseware-content-releases-app.js' ).then(({ default: mountApp }) => { return mountApp(STUDIP, createApp, '#courseware-content-releases-app'); diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index f7ee74e..ab114ad 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -94,6 +94,12 @@ c o n t e n t s * * * * * * * * */ .cw-content-overview { max-width: 1100px; + h2 { + margin: 0; + font-weight: 400; + padding: 5px 0; + font-size: 1.4em; + } } .cw-contents-overview-teaser { @@ -193,6 +199,22 @@ c o n t e n t s } } +.cw-content-courses { + h2 { + margin: 0; + font-weight: 400; + padding: 5px 0; + font-size: 1.4em; + } + ul.cw-tiles { + margin-bottom: 20px; + } +} + +.cw-contents-overview-personal { + margin-bottom: 2em; +} + /* * * * * * * * * * * c o n t e n t s e n d * * * * * * * * * * */ @@ -202,7 +224,8 @@ r i b b o n * * * * * */ $consum_ribbon_width: calc(100% - 58px); #course-courseware-index, -#contents-courseware-courseware { +#contents-courseware-courseware, +#contents-courseware-shared_content_courseware { &.consume { overflow: hidden; } @@ -725,6 +748,23 @@ ribbon end padding: 0; font-size: 1.25em; } + td.perm { + input.right, input.date { + cursor: pointer !important; + } + } + } + button.cw-add-persons { + margin-left: 4px; + } + button.cw-permission-delete { + width: 24px; + height: 24px; + border: none; + background-color: transparent; + @include background-icon(trash, clickable); + background-repeat: no-repeat; + cursor: pointer; } } @@ -3117,6 +3157,26 @@ a u d i o b l o c k a u d i o b l o c k e n d * * * * * * * * * * * * * */ +/* * * * * * * * * * * * * * * * * * * * +f o r m u l t i m e d i a b l o c k s +* * * * * * * * * * * * * * * * * * * */ +.cw-file-empty { + @include background-icon(file, info, 96); + border: solid thin $content-color-40; + background-position: center 1em; + background-repeat: no-repeat; + min-height: 140px; + padding: 1em; + p { + text-align: center; + padding-top: 106px; + } +} + +/* * * * * * * * * * * * * * * * * * * * * * * * +f o r m u l t i m e d i a b l o c k s e n d +* * * * * * * * * * * * * * * * * * * * * * * */ + /* * * * * * * * * * v i d e o b l o c k * * * * * * * * * * */ @@ -4808,17 +4868,6 @@ cw tiles end } /* courseware template preview end*/ -/* contents courseware courses */ -.cw-content-courses { - h2 { - margin-top: 0; - } - ul.cw-tiles { - margin-bottom: 20px; - } -} -/* contents courseware courses end*/ - /* * * * * * * * * * i n p u t f i l e * * * * * * * * * */ diff --git a/resources/assets/stylesheets/scss/multi_person_search.scss b/resources/assets/stylesheets/scss/multi_person_search.scss new file mode 100755 index 0000000..e1e6270 --- /dev/null +++ b/resources/assets/stylesheets/scss/multi_person_search.scss @@ -0,0 +1,8 @@ +.studip-msp-vue { + a.msp-btn { + margin-left: 5px; + img { + vertical-align: middle; + } + } +} diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index deca838..1b0d2e0 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -99,6 +99,7 @@ @import "scss/typography"; @import "scss/user-administration"; @import "scss/wiki"; +@import "scss/multi_person_search"; // Class for DOM elements that should only be visible to Screen readers diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js index 09dbac3..ccff7c5 100644 --- a/resources/vue/base-components.js +++ b/resources/vue/base-components.js @@ -19,6 +19,7 @@ import StudipProxyCheckbox from './components/StudipProxyCheckbox.vue'; import StudipProxiedCheckbox from './components/StudipProxiedCheckbox.vue'; import StudipTooltipIcon from './components/StudipTooltipIcon.vue'; import StudipSelect from './components/StudipSelect.vue'; +import StudipMultiPersonSearch from './components/StudipMultiPersonSearch.vue'; const BaseComponents = { Multiselect, @@ -41,7 +42,8 @@ const BaseComponents = { StudipProxiedCheckbox, StudipTooltipIcon, StudipSelect, - TextareaWithToolbar + TextareaWithToolbar, + StudipMultiPersonSearch }; export default BaseComponents; diff --git a/resources/vue/components/StudipIcon.vue b/resources/vue/components/StudipIcon.vue index 21b37a2..3a2ac79 100644 --- a/resources/vue/components/StudipIcon.vue +++ b/resources/vue/components/StudipIcon.vue @@ -28,7 +28,8 @@ if (this.shape.indexOf("http") === 0) { return this.shape; } - return `${STUDIP.ASSETS_URL}images/icons/${this.color}/${this.shape}.svg`; + var path = this.shape.split('+').reverse().join('/'); + return `${STUDIP.ASSETS_URL}images/icons/${this.color}/${path}.svg`; }, color: function () { switch (this.role) { diff --git a/resources/vue/components/StudipMultiPersonSearch.vue b/resources/vue/components/StudipMultiPersonSearch.vue new file mode 100644 index 0000000..17a70cf --- /dev/null +++ b/resources/vue/components/StudipMultiPersonSearch.vue @@ -0,0 +1,190 @@ + + + diff --git a/resources/vue/components/courseware/ContentOverviewApp.vue b/resources/vue/components/courseware/ContentOverviewApp.vue index cad3fc0..831a2e7 100644 --- a/resources/vue/components/courseware/ContentOverviewApp.vue +++ b/resources/vue/components/courseware/ContentOverviewApp.vue @@ -1,10 +1,10 @@ \ No newline at end of file + diff --git a/resources/vue/components/courseware/CoursewareContentPermissions.vue b/resources/vue/components/courseware/CoursewareContentPermissions.vue new file mode 100755 index 0000000..2264072 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareContentPermissions.vue @@ -0,0 +1,391 @@ + + diff --git a/resources/vue/components/courseware/CoursewareContentShared.vue b/resources/vue/components/courseware/CoursewareContentShared.vue new file mode 100644 index 0000000..19594d0 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareContentShared.vue @@ -0,0 +1,173 @@ + + + \ No newline at end of file diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue index 55dd416..bdd0196 100644 --- a/resources/vue/components/courseware/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue @@ -179,7 +179,7 @@ :closeText="textEdit.close" closeClass="cancel" height="500" - width="500" + :width="inContent ? '720' : '500'" class="studip-dialog-with-tab" @close="closeEditDialog" @confirm="storeCurrentElement" @@ -305,6 +305,12 @@ @updateReadApproval="updateReadApproval" @updateWriteApproval="updateWriteApproval" /> +
@@ -599,6 +605,7 @@ import ContainerComponents from './container-components.js'; import CoursewarePluginComponents from './plugin-components.js'; import CoursewareStructuralElementPermissions from './CoursewareStructuralElementPermissions.vue'; +import CoursewareContentPermissions from './CoursewareContentPermissions.vue'; import CoursewareStructuralElementDiscussion from './CoursewareStructuralElementDiscussion.vue'; import CoursewareAccordionContainer from './CoursewareAccordionContainer.vue'; import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; @@ -623,6 +630,7 @@ export default { components: { CoursewareStructuralElementDiscussion, CoursewareStructuralElementPermissions, + CoursewareContentPermissions, CoursewareRibbon, CoursewareListContainer, CoursewareAccordionContainer, @@ -758,6 +766,11 @@ export default { return this.$store.getters.context.type === 'courses'; }, + inContent() { + // The rights tab in contents will be only visible to the owner. + return this.$store.getters.context.type === 'users' && this.userId === this.currentElement.relationships.user.data.id; + }, + textDelete() { let textDelete = {}; textDelete.title = this.$gettext('Seite unwiderruflich löschen'); @@ -790,6 +803,11 @@ export default { valid = true; } } + if (context.type === 'sharedusers') { + if (context.id === this.courseware.relationships.root.data.id) { + valid = true; + } + } if (context.type === 'public') { valid = true; @@ -925,11 +943,11 @@ export default { menu.push({ id: 3, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' }); } if (this.context.type === 'users') { - menu.push({ id: 6, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' }); + menu.push({ id: 7, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' }); } if (!this.isRoot && this.canEdit && !this.isTask) { menu.push({ - id: 9, + id: 8, label: this.$gettext('Seite löschen'), icon: 'trash', emit: 'deleteCurrentElement', diff --git a/resources/vue/components/courseware/CoursewareTreeItem.vue b/resources/vue/components/courseware/CoursewareTreeItem.vue index b372b17..4e35977 100644 --- a/resources/vue/components/courseware/CoursewareTreeItem.vue +++ b/resources/vue/components/courseware/CoursewareTreeItem.vue @@ -112,6 +112,9 @@ export default { return writeApproval.all || writeApproval.groups.length > 0 || writeApproval.users.length > 0; }, hasNoReadApproval() { + if (this.context.type === 'users') { + return false; + } const readApproval = this.element.attributes['read-approval']; if (Object.keys(readApproval).length === 0 || this.hasWriteApproval) { diff --git a/resources/vue/courseware-content-overview-app.js b/resources/vue/courseware-content-overview-app.js index e1f15ef..ccacce8 100644 --- a/resources/vue/courseware-content-overview-app.js +++ b/resources/vue/courseware-content-overview-app.js @@ -37,6 +37,7 @@ const mountApp = (STUDIP, createApp, element) => { 'courseware-containers', 'courseware-instances', 'courseware-structural-elements', + 'courseware-structural-elements-shared', 'courseware-templates', 'courseware-user-data-fields', 'courseware-user-progresses', @@ -84,6 +85,8 @@ const mountApp = (STUDIP, createApp, element) => { type: entry_type, }); + store.dispatch('courseware-structural-elements-shared/loadAll', { options: { include: 'owner' } }); + const app = createApp({ render: (h) => h(ContentOverviewApp), store diff --git a/resources/vue/courseware-content-releases-app.js b/resources/vue/courseware-content-releases-app.js index f99744b..79d56cc 100644 --- a/resources/vue/courseware-content-releases-app.js +++ b/resources/vue/courseware-content-releases-app.js @@ -23,6 +23,7 @@ const mountApp = (STUDIP, createApp, element) => { 'courseware-containers', 'courseware-public-links', 'courseware-structural-elements', + 'courseware-structural-elements-released', 'file-refs', 'users', ], @@ -46,6 +47,7 @@ const mountApp = (STUDIP, createApp, element) => { } } + store.dispatch('setUserId', STUDIP.USER_ID); store.dispatch('coursewareContext', { id: entry_id, type: entry_type, @@ -56,6 +58,7 @@ const mountApp = (STUDIP, createApp, element) => { include: 'structural-element', }, }); + store.dispatch('courseware-structural-elements-released/loadAll', {}); const app = createApp({ render: (h) => h(ContentReleasesApp), @@ -67,4 +70,4 @@ const mountApp = (STUDIP, createApp, element) => { return app; } -export default mountApp; \ No newline at end of file +export default mountApp; diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index f838760..0c4811e 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -50,7 +50,9 @@ const getDefaultState = () => { exportState: '', exportProgress: 0, + permissionFilter: 'read', purposeFilter: 'all', + sourceFilter: 'all', showOverviewElementAddDialog: false, bookmarkFilter: 'all', @@ -201,9 +203,15 @@ const getters = { exportProgress(state) { return state.exportProgress; }, + permissionFilter(state) { + return state.permissionFilter; + }, purposeFilter(state) { return state.purposeFilter; }, + sourceFilter(state) { + return state.sourceFilter; + }, bookmarkFilter(state) { return state.bookmarkFilter; }, @@ -1226,9 +1234,15 @@ export const actions = { await dispatch('courseware-task-feedback/delete', data, { root: true }); }, + setPermissionFilter({ commit }, permission) { + commit('setPermissionFilter', permission); + }, setPurposeFilter({ commit }, purpose) { commit('setPurposeFilter', purpose); }, + setSourceFilter({ commit }, source) { + commit('setSourceFilter', source); + }, setBookmarkFilter({ commit }, course) { commit('setBookmarkFilter', course); }, @@ -1413,9 +1427,15 @@ export const mutations = { setExportProgress(state, exportProgress) { state.exportProgress = exportProgress; }, + setPermissionFilter(state, permission) { + state.permissionFilter = permission; + }, setPurposeFilter(state, purpose) { state.purposeFilter = purpose; }, + setSourceFilter(state, source) { + state.sourceFilter = source; + }, setBookmarkFilter(state, course) { state.bookmarkFilter = course; }, -- cgit v1.0