aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRon Lucke <lucke@elan-ev.de>2022-09-26 08:11:22 +0000
committerMarcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>2022-09-26 08:11:22 +0000
commitec684bbd1629803bee4c15faf78c9997aaf7daf5 (patch)
treeff7f8145f3d12469aad4839e16cc77f82d0cced7
parent93fb3b44a2f1f4af9e568041a579fda27fa86e6e (diff)
StEP00362: Rechte- und Zugriffsverwaltung für Arbeitsplatz > Lernmaterialien
Closes #919 Merge request studip/studip!639
-rw-r--r--app/controllers/contents/courseware.php37
-rw-r--r--app/controllers/multipersonsearch.php40
-rwxr-xr-xapp/views/contents/courseware/shared_content_courseware.php10
-rw-r--r--lib/classes/JsonApi/RouteMap.php6
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/Authority.php61
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php1
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/StructuralElementsReleasedIndex.php59
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/StructuralElementsSharedIndex.php77
-rw-r--r--lib/models/Courseware/StructuralElement.php166
-rw-r--r--lib/navigation/ContentsNavigation.php54
-rw-r--r--resources/assets/javascripts/bootstrap/courseware.js2
-rw-r--r--resources/assets/stylesheets/scss/courseware.scss73
-rwxr-xr-xresources/assets/stylesheets/scss/multi_person_search.scss8
-rw-r--r--resources/assets/stylesheets/studip.scss1
-rw-r--r--resources/vue/base-components.js4
-rw-r--r--resources/vue/components/StudipIcon.vue3
-rw-r--r--resources/vue/components/StudipMultiPersonSearch.vue190
-rw-r--r--resources/vue/components/courseware/ContentOverviewApp.vue4
-rw-r--r--resources/vue/components/courseware/ContentReleasesApp.vue6
-rw-r--r--resources/vue/components/courseware/CoursewareContentOverviewElements.vue504
-rw-r--r--resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue106
-rwxr-xr-xresources/vue/components/courseware/CoursewareContentPermissions.vue391
-rw-r--r--resources/vue/components/courseware/CoursewareContentShared.vue173
-rw-r--r--resources/vue/components/courseware/CoursewareStructuralElement.vue24
-rw-r--r--resources/vue/components/courseware/CoursewareTreeItem.vue3
-rw-r--r--resources/vue/courseware-content-overview-app.js3
-rw-r--r--resources/vue/courseware-content-releases-app.js5
-rw-r--r--resources/vue/store/courseware/courseware.module.js20
28 files changed, 1704 insertions, 327 deletions
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 @@
+<div
+ id="courseware-index-app"
+ entry-element-id="<?= $entry_element_id ?>"
+ entry-type="sharedusers"
+ entry-id="<?= $entry_element_id ?>"
+ oer-enabled='<?= $oer_enabled ?>'
+ oer-title="<?= Config::get()->OER_TITLE ?>"
+ licenses='<?= $licenses ?>'
+ >
+</div>
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 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElement;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Class StructuralElementsReleasedIndex.
+ */
+class StructuralElementsReleasedIndex extends JsonApiController
+{
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ protected $allowedIncludePaths = [
+ 'ancestors',
+ 'children',
+ 'containers',
+ 'containers.blocks',
+ 'containers.blocks.edit-blocker',
+ 'containers.blocks.editor',
+ 'containers.blocks.owner',
+ 'containers.blocks.user-data-field',
+ 'containers.blocks.user-progress',
+ 'course',
+ 'editor',
+ 'owner',
+ 'parent',
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $user = $this->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 @@
+<?php
+
+namespace JsonApi\Routes\Courseware;
+
+use Courseware\StructuralElement;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Class StructuralElementsSharedIndex.
+ */
+class StructuralElementsSharedIndex extends JsonApiController
+{
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ protected $allowedIncludePaths = [
+ 'ancestors',
+ 'children',
+ 'containers',
+ 'containers.blocks',
+ 'containers.blocks.edit-blocker',
+ 'containers.blocks.editor',
+ 'containers.blocks.owner',
+ 'containers.blocks.user-data-field',
+ 'containers.blocks.user-progress',
+ 'course',
+ 'editor',
+ 'owner',
+ 'parent',
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $user = $this->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 @@
+<template>
+ <div class="mpscontainer studip-msp-vue">
+ <form method="post" class="default" @submit.prevent="search">
+ <label class="with-action">
+ <input type="text" ref="searchInputField" v-model="searchTerm" :placeholder="$gettext('Suchen')" style="width: 260px;">
+ <a href="#" class="msp-btn" @click.prevent="search" :title="$gettext('Suche starten')">
+ <studip-icon shape="search" role="clickable" size="16"></studip-icon>
+ </a>
+ <a href="#" class="msp-btn" @click.prevent="resetSearch" :title="$gettext('Suche zurücksetzen')">
+ <studip-icon shape="decline" role="clickable" size="16"></studip-icon>
+ </a>
+ </label>
+ <select multiple="multiple" :id="select_box_id" name="selectbox[]"></select>
+ </form>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'studip-multi-person-search',
+ props: {
+ name: String,
+ withDetail: {
+ type: Boolean,
+ default: true
+ }
+ },
+ data() {
+ return {
+ searchTerm: '',
+ count: 0,
+ users: []
+ }
+ },
+ mounted () {
+ this.$nextTick(() => {
+ this.init();
+ setTimeout(() => {
+ this.$refs.searchInputField.focus();
+ }, 100);
+ });
+ },
+ computed: {
+ id() {
+ return this._uid;
+ },
+ count_text_id() {
+ return this.id + '_count';
+ },
+ select_box_id() {
+ return this.id + '_selectbox';
+ },
+ },
+ methods: {
+ init() {
+ let select_all_btn = document.createElement('a');
+ select_all_btn.setAttribute('id', `${this.id}-select-all`);
+ select_all_btn.setAttribute('href', '#');
+ select_all_btn.innerText = this.$gettext('Alle hinzufügen');
+ select_all_btn.addEventListener('click', (e) => {
+ e.preventDefault();
+ this.selectAll();
+ });
+ let unselect_all_btn = document.createElement('a');
+ unselect_all_btn.setAttribute('id', `${this.id}-unselect-all`);
+ unselect_all_btn.setAttribute('href', '#');
+ unselect_all_btn.innerText = this.$gettext('Alle entfernen');
+ unselect_all_btn.addEventListener('click', (e) => {
+ e.preventDefault();
+ this.unselectAll();
+ });
+ let selection_header = document.createElement('div');
+ selection_header.setAttribute('id', this.count_text_id);
+ selection_header.innerText = this.$gettextInterpolate('Sie haben %{ count } Personen ausgewählt', {count: this.count});
+
+ $('#' + this.select_box_id).multiSelect({
+ selectableHeader: '<div>' + this.$gettext('Suchergebnisse') + '</div>',
+ selectionHeader: selection_header,
+ selectableFooter: select_all_btn,
+ selectionFooter: unselect_all_btn,
+ afterSelect: () => this.updateSelection(),
+ afterDeselect: () => this.updateSelection()
+ });
+ },
+
+ search() {
+ this.users = [];
+ let view = this;
+ $.getJSON(
+ STUDIP.URLHelper.getURL('dispatch.php/multipersonsearch/ajax_search_vue/' + this.name, { s: this.searchTerm }),
+ function(data) {
+ view.removeAllNotSelected();
+ var searchcount = 0;
+ $.each(data, function(i, item) {
+ searchcount += view.append(
+ item.id,
+ item.avatar + ' -- ' + item.text,
+ item.selected
+ );
+ delete item.selected;
+ view.users.push(item);
+ });
+ view.refresh();
+
+ if (searchcount === 0) {
+ view.append(
+ '--',
+ view.$gettextInterpolate('Es wurden keine neuen Ergebnisse für "%{ needle }" gefunden.', {needle: view.searchTerm}),
+ true
+ );
+ view.refresh();
+ }
+ }
+ );
+ },
+
+ selectAll: function() {
+ $('#' + this.select_box_id).multiSelect('select_all');
+ this.updateSelection();
+ },
+
+ unselectAll: function() {
+ $('#' + this.select_box_id).multiSelect('deselect_all');
+ this.updateSelection();
+ },
+
+ removeAll: function() {
+ $('#' + this.select_box_id + ' option').remove();
+ this.refresh();
+ },
+
+ removeAllNotSelected() {
+ $('#' + this.select_box_id + ' option:not(:selected)').remove();
+ this.refresh();
+ },
+
+ resetSearch() {
+ this.searchTerm = '';
+ this.removeAllNotSelected();
+ },
+
+ append(id, text, selected = false) {
+ if ($('#' + this.select_box_id + ' option[value=' + id + ']').length === 0) {
+ $('#' + this.select_box_id).multiSelect('addOption', {
+ value: id,
+ text: text,
+ disabled: selected
+ });
+ return 1;
+ }
+ return 0;
+ },
+
+ refresh() {
+ $('#' + this.select_box_id).multiSelect('refresh');
+ this.updateSelection();
+ },
+
+ updateCount(){
+ this.count = $('#' + this.select_box_id + ' option:enabled:selected').length;
+ $('#' + this.count_text_id).text(this.$gettextInterpolate('Sie haben %{ count } Personen ausgewählt', {count: this.count}));
+ },
+
+ async updateSelection() {
+ this.updateCount();
+ let selected_options = $('#' + this.select_box_id + ' option:enabled:selected');
+ let user_ids = [];
+ if (selected_options.length) {
+ for (const option of selected_options) {
+ user_ids.push(option.value);
+ }
+ }
+ let return_value = [];
+ if (this.withDetail && this.users.length) {
+ for (const user_id of user_ids) {
+ let existing_index = this.users.findIndex(user => {
+ return user.id === user_id;
+ });
+ if (existing_index !== -1) {
+ return_value.push(this.users[existing_index]);
+ }
+ }
+ } else {
+ return_value = user_ids;
+ }
+ this.$emit('input', return_value);
+ }
+ },
+}
+</script>
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 @@
<template>
<div class="cw-content-overview">
<courseware-content-overview-elements />
- <MountingPortal mountTo="#courseware-content-overview-action-widget" name="sidebar-views">
+ <MountingPortal mountTo="#courseware-content-overview-action-widget" name="sidebar-actions">
<courseware-content-overview-action-widget />
</MountingPortal>
- <MountingPortal mountTo="#courseware-content-overview-filter-widget" name="sidebar-views">
+ <MountingPortal mountTo="#courseware-content-overview-filter-widget" name="sidebar-filters">
<courseware-content-overview-filter-widget />
</MountingPortal>
</div>
diff --git a/resources/vue/components/courseware/ContentReleasesApp.vue b/resources/vue/components/courseware/ContentReleasesApp.vue
index 7dd5500..1a2ed6f 100644
--- a/resources/vue/components/courseware/ContentReleasesApp.vue
+++ b/resources/vue/components/courseware/ContentReleasesApp.vue
@@ -1,18 +1,22 @@
<template>
<div class="cw-content-releases">
<courseware-content-links />
+ <courseware-content-shared />
<courseware-companion-overlay />
</div>
</template>
<script>
import CoursewareContentLinks from './CoursewareContentLinks.vue';
+import CoursewareContentShared from './CoursewareContentShared.vue';
import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
export default {
components: {
CoursewareContentLinks,
+ CoursewareContentShared,
CoursewareCompanionOverlay
- }
+ },
+
}
</script>
diff --git a/resources/vue/components/courseware/CoursewareContentOverviewElements.vue b/resources/vue/components/courseware/CoursewareContentOverviewElements.vue
index 0958475..c23e6d6 100644
--- a/resources/vue/components/courseware/CoursewareContentOverviewElements.vue
+++ b/resources/vue/components/courseware/CoursewareContentOverviewElements.vue
@@ -1,214 +1,267 @@
<template>
-<div v-if="root">
- <ul class="cw-tiles">
- <li
- v-for="child in filteredChildren"
- :key="child.id"
- class="tile"
- :class="[child.attributes.payload.color, filteredChildren.length > 3 ? '': 'cw-tile-margin']"
- >
- <a :href="getElementUrl(child.id)" :title="child.attributes.title">
- <div
- class="preview-image"
- :class="[hasImage(child) ? '' : 'default-image']"
- :style="getChildStyle(child)"
- ></div>
- <div class="description">
- <header
- :class="[child.attributes.purpose !== '' ? 'description-icon-' + child.attributes.purpose : '']"
- >
- {{ child.attributes.title }}
- </header>
- <div class="description-text-wrapper">
- <p>{{ child.attributes.payload.description }}</p>
- </div>
- <footer>
- {{ countChildren(child) + 1 }}
- <translate
- :translate-n="countChildren(child) + 1"
- translate-plural="Seiten"
- >
- Seite
- </translate>
- </footer>
- </div>
- </a>
- </li>
- </ul>
- <courseware-companion-box v-if="children.length !== 0 && filteredChildren.length === 0 && purposeFilter !== 'all'" :msgCompanion="text.emptyFilter" mood="pointing"/>
- <div v-if="children.length === 0" class="cw-contents-overview-teaser">
- <div class="cw-contents-overview-teaser-content">
- <header><translate>Ihre persönlichen Lernmaterialien</translate></header>
- <p><translate>Erstellen und Verwalten Sie hier ihre eigenen persönlichen Lernmaterialien in Form von ePorfolios,
- Vorlagen für Veranstaltungen oder einfach nur persönliche Inhalte für das Studium.
- Entwickeln Sie ihre eigenen (Lehr-)Materialien für Studium oder die Lehre und teilen diese mit anderen Nutzenden.</translate></p>
- <button class="button" @click="addElement">
- <translate>Neues Lernmaterial anlegen</translate>
- </button>
- </div>
- </div>
- <studip-dialog
- v-if="showOverviewElementAddDialog"
- :title="$gettext('Neues Lernmaterial anlegen')"
- height="600"
- width="500"
- :confirmText="$gettext('Erstellen')"
- confirmClass="accept"
- :closeText="$gettext('Schließen')"
- closeClass="cancel"
- class="cw-structural-element-dialog"
- @close="closeAddDialog"
- @confirm="createElement"
- >
- <template v-slot:dialogContent>
-
- <courseware-collapsible-box
- :title="$gettext('Grundeinstellungen')"
- :open="true"
+ <div class="cw-contents-overview-wrapper">
+ <div v-if="root && filteredChildren.length > 0" class="cw-contents-overview-personal">
+ <h2>
+ <translate>Persönliche Lernmaterialien</translate>
+ </h2>
+ <ul class="cw-tiles">
+ <li
+ v-for="child in filteredChildren"
+ :key="child.id"
+ class="tile"
+ :class="[child.attributes.payload.color, filteredChildren.length > 3 ? '': 'cw-tile-margin']"
>
- <form class="default" @submit.prevent="">
- <label>
- <translate>Titel des Lernmaterials</translate><br />
- <input v-model="newElement.attributes.title" type="text" />
- </label>
- <label>
- <translate>Zusammenfassung</translate><br />
- <textarea v-model="newElement.attributes.payload.description"></textarea>
- </label>
- <label>
- <translate>Bild</translate>
- <br>
- <input ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
- <courseware-companion-box
- v-if="uploadFileError"
- :msgCompanion="uploadFileError"
- mood="sad"
- class="cw-companion-box-in-form"
- />
- </label>
- <label>
- <translate>Art des Lernmaterials</translate>
- <select v-model="newElementPurpose">
- <option value="content"><translate>Inhalt</translate></option>
- <option 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>
- <label>
- <translate>Lernmaterialvorlage</translate>
- <select v-model="newElementTemplate">
- <option :value="null"><translate>ohne Vorlage</translate></option>
- <option
- v-for="template in selectableTemplates"
- :key="template.id"
- :value="template"
- >
- {{ template.attributes.name }}
- </option>
- </select>
- </label>
- </form>
- </courseware-collapsible-box>
- <courseware-collapsible-box :title="$gettext('Vorschau')">
- <div v-if="currentTemplateStructure" class="cw-template-preview">
+ <a :href="getElementUrl(child.id)" :title="child.attributes.title">
<div
- class="cw-template-preview-container-wrapper"
- v-for="container in currentTemplateStructure.containers"
- :key="container.id"
- :class="['cw-template-preview-container-' + container.attributes.payload.colspan]"
- >
- <div class="cw-template-preview-container-content">
- <header class="cw-template-preview-container-title">
- {{ container.attributes.title }} | {{ container.attributes.width }}
- </header>
- <div class="cw-template-preview-blocks" v-for="block in container.blocks" :key="block.id">
- <header class="cw-template-preview-blocks-title">
- {{ block.attributes.title }}
+ class="preview-image"
+ :class="[hasImage(child) ? '' : 'default-image']"
+ :style="getChildStyle(child)"
+ ></div>
+ <div class="description">
+ <header
+ :class="[child.attributes.purpose !== '' ? 'description-icon-' + child.attributes.purpose : '']"
+ >
+ {{ child.attributes.title }}
+ </header>
+ <div class="description-text-wrapper">
+ <p>{{ child.attributes.payload.description }}</p>
+ </div>
+ <footer>
+ {{ countChildren(child) + 1 }}
+ <translate
+ :translate-n="countChildren(child) + 1"
+ translate-plural="Seiten"
+ >
+ Seite
+ </translate>
+ </footer>
+ </div>
+ </a>
+ </li>
+ </ul>
+ </div>
+ <div v-if="children.length === 0" class="cw-contents-overview-teaser">
+ <div class="cw-contents-overview-teaser-content">
+ <header><translate>Ihre persönlichen Lernmaterialien</translate></header>
+ <p><translate>Erstellen und Verwalten Sie hier ihre eigenen persönlichen Lernmaterialien in Form von ePorfolios,
+ Vorlagen für Veranstaltungen oder einfach nur persönliche Inhalte für das Studium.
+ Entwickeln Sie ihre eigenen (Lehr-)Materialien für Studium oder die Lehre und teilen diese mit anderen Nutzenden.</translate></p>
+ <button class="button" @click="addElement">
+ <translate>Neues Lernmaterial anlegen</translate>
+ </button>
+ </div>
+ </div>
+ <studip-dialog
+ v-if="showOverviewElementAddDialog"
+ :title="$gettext('Neues Lernmaterial anlegen')"
+ height="600"
+ width="500"
+ :confirmText="$gettext('Erstellen')"
+ confirmClass="accept"
+ :closeText="$gettext('Schließen')"
+ closeClass="cancel"
+ class="cw-structural-element-dialog"
+ @close="closeAddDialog"
+ @confirm="createElement"
+ >
+ <template v-slot:dialogContent>
+
+ <courseware-collapsible-box
+ :title="$gettext('Grundeinstellungen')"
+ :open="true"
+ >
+ <form class="default" @submit.prevent="">
+ <label>
+ <translate>Titel des Lernmaterials</translate><br />
+ <input v-model="newElement.attributes.title" type="text" />
+ </label>
+ <label>
+ <translate>Zusammenfassung</translate><br />
+ <textarea v-model="newElement.attributes.payload.description"></textarea>
+ </label>
+ <label>
+ <translate>Bild</translate>
+ <br>
+ <input ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
+ <courseware-companion-box
+ v-if="uploadFileError"
+ :msgCompanion="uploadFileError"
+ mood="sad"
+ class="cw-companion-box-in-form"
+ />
+ </label>
+ <label>
+ <translate>Art des Lernmaterials</translate>
+ <select v-model="newElementPurpose">
+ <option value="content"><translate>Inhalt</translate></option>
+ <option 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>
+ <label>
+ <translate>Lernmaterialvorlage</translate>
+ <select v-model="newElementTemplate">
+ <option :value="null"><translate>ohne Vorlage</translate></option>
+ <option
+ v-for="template in selectableTemplates"
+ :key="template.id"
+ :value="template"
+ >
+ {{ template.attributes.name }}
+ </option>
+ </select>
+ </label>
+ </form>
+ </courseware-collapsible-box>
+ <courseware-collapsible-box :title="$gettext('Vorschau')">
+ <div v-if="currentTemplateStructure" class="cw-template-preview">
+ <div
+ class="cw-template-preview-container-wrapper"
+ v-for="container in currentTemplateStructure.containers"
+ :key="container.id"
+ :class="['cw-template-preview-container-' + container.attributes.payload.colspan]"
+ >
+ <div class="cw-template-preview-container-content">
+ <header class="cw-template-preview-container-title">
+ {{ container.attributes.title }} | {{ container.attributes.width }}
</header>
+ <div class="cw-template-preview-blocks" v-for="block in container.blocks" :key="block.id">
+ <header class="cw-template-preview-blocks-title">
+ {{ block.attributes.title }}
+ </header>
+ </div>
</div>
</div>
</div>
- </div>
- <courseware-companion-box
- v-else
- :msgCompanion="$gettext('Sie können eine Lernmaterialvorlage auswählen und hier eine Vorschau betrachten. Ohne Vorlage wird eine leere Seite erzeugt.')"
- />
- </courseware-collapsible-box>
- <courseware-collapsible-box
- :title="$gettext('Zusatzangaben')"
- >
- <form class="default" @submit.prevent="">
- <label>
- <translate>Lizenztyp</translate>
- <select v-model="newElement.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="newElement.attributes.payload.required_time" />
- </label>
- <label>
- <translate>Niveau</translate><br />
- <translate>von</translate>
- <select v-model="newElement.attributes.payload.difficulty_start">
- <option
- v-for="difficulty_start in 12"
- :key="difficulty_start"
- :value="difficulty_start"
- >
- {{ difficulty_start }}
- </option>
- </select>
- <translate>bis</translate>
- <select v-model="newElement.attributes.payload.difficulty_end">
- <option
- v-for="difficulty_end in 12"
- :key="difficulty_end"
- :value="difficulty_end"
+ <courseware-companion-box
+ v-else
+ :msgCompanion="$gettext('Sie können eine Lernmaterialvorlage auswählen und hier eine Vorschau betrachten. Ohne Vorlage wird eine leere Seite erzeugt.')"
+ />
+ </courseware-collapsible-box>
+ <courseware-collapsible-box
+ :title="$gettext('Zusatzangaben')"
+ >
+ <form class="default" @submit.prevent="">
+ <label>
+ <translate>Lizenztyp</translate>
+ <select v-model="newElement.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="newElement.attributes.payload.required_time" />
+ </label>
+ <label>
+ <translate>Niveau</translate><br />
+ <translate>von</translate>
+ <select v-model="newElement.attributes.payload.difficulty_start">
+ <option
+ v-for="difficulty_start in 12"
+ :key="difficulty_start"
+ :value="difficulty_start"
+ >
+ {{ difficulty_start }}
+ </option>
+ </select>
+ <translate>bis</translate>
+ <select v-model="newElement.attributes.payload.difficulty_end">
+ <option
+ v-for="difficulty_end in 12"
+ :key="difficulty_end"
+ :value="difficulty_end"
+ >
+ {{ difficulty_end }}
+ </option>
+ </select>
+ </label>
+ <label>
+ <translate>Farbe</translate>
+ <studip-select
+ v-model="newElement.attributes.payload.color"
+ :options="colors"
+ :reduce="(color) => color.class"
+ label="class"
>
- {{ difficulty_end }}
- </option>
- </select>
- </label>
- <label>
- <translate>Farbe</translate>
- <v-select
- v-model="newElement.attributes.payload.color"
- :options="colors"
- :reduce="(color) => color.class"
- label="class"
+ <template #open-indicator="selectAttributes">
+ <span v-bind="selectAttributes"
+ ><studip-icon shape="arr_1down" size="10"
+ /></span>
+ </template>
+ <template #no-options="{ search, searching, loading }">
+ <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>
+ </form>
+ </courseware-collapsible-box>
+
+ </template>
+ </studip-dialog>
+
+ <div v-if="filteredShared.length > 0" class="cw-contents-overview-shared">
+ <h2>
+ <translate>Geteilte Lernmaterialien</translate>
+ </h2>
+ <ul class="cw-tiles">
+ <li
+ v-for="element in filteredShared"
+ :key="element.id"
+ class="tile"
+ :class="[element.attributes.payload.color, sharedElements.length > 3 ? '': 'cw-tile-margin']"
+ >
+ <a :href="getSharedElementUrl(element.id)" :title="element.attributes.title">
+ <div
+ class="preview-image"
+ :class="[hasImage(element) ? '' : 'default-image']"
+ :style="getChildStyle(element)"
+ >
+ <div class="overlay-text">{{ getOwnerName(element) }}</div>
+ </div>
+ <div class="description">
+ <header
+ :class="[element.attributes.purpose !== '' ? 'description-icon-' + element.attributes.purpose : '']"
>
- <template #open-indicator="selectAttributes">
- <span v-bind="selectAttributes"
- ><studip-icon shape="arr_1down" size="10"
- /></span>
- </template>
- <template #no-options="{ search, searching, loading }">
- <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>
- </v-select>
- </label>
- </form>
- </courseware-collapsible-box>
+ {{ element.attributes.title }}
+ </header>
+ <div class="description-text-wrapper">
+ <p>{{ element.attributes.payload.description }}</p>
+ </div>
+ <footer>
+ {{ countChildren(element) + 1 }}
+ <translate
+ :translate-n="countChildren(element) + 1"
+ translate-plural="Seiten"
+ >
+ Seite
+ </translate>
+ </footer>
+ </div>
+ </a>
+ </li>
+ </ul>
+ </div>
+ <courseware-companion-box
+ v-if="children.length !== 0 && filteredChildren.length === 0 && sharedElements.length !== 0 && filteredShared.length === 0"
+ :msgCompanion="$gettext('Für diese Auswahl wurden keine Lernmaterialien gefunden.')"
+ mood="pointing"
+ />
- </template>
- </studip-dialog>
- <courseware-companion-overlay />
-</div>
+ <courseware-companion-overlay />
+ </div>
</template>
<script>
@@ -228,10 +281,6 @@ export default {
},
data() {
return {
- text: {
- emptyFilter: this.$gettext('Für diese Auswahl wurden keine Lernmaterialien gefunden.'),
- empty: this.$gettext('Es wurden keine Lernmaterialien gefunden.'),
- },
newElement: {
attributes: {
payload: {},
@@ -246,9 +295,13 @@ export default {
...mapGetters({
getElement: 'courseware-structural-elements/byId',
licenses: 'licenses',
+ permissionFilter: 'permissionFilter',
purposeFilter: 'purposeFilter',
+ sourceFilter: 'sourceFilter',
showOverviewElementAddDialog: 'showOverviewElementAddDialog',
templates: 'courseware-templates/all',
+ sharedElements: 'courseware-structural-elements-shared/all',
+ userById: 'users/byId',
}),
root() {
return this.getElement({id: STUDIP.COURSEWARE_USERS_ROOT_ID});
@@ -266,10 +319,32 @@ export default {
return children;
},
filteredChildren() {
+ if (!['all', 'personal'].includes(this.sourceFilter)) {
+ return [];
+ }
+ let children = this.children;
+ if (this.purposeFilter !== 'all') {
+ children = children.filter(child => { return child.attributes.purpose === this.purposeFilter});
+ }
+ if (this.permissionFilter !== 'read') {
+ children = children.filter(child => { return child.attributes['can-edit'] });
+ }
+
+ return children;
+ },
+ filteredShared() {
+ if (!['all', 'shared'].includes(this.sourceFilter)) {
+ return [];
+ }
+ let elements = this.sharedElements;
if (this.purposeFilter !== 'all') {
- return this.children.filter(child => { return child.attributes.purpose === this.purposeFilter});
+ elements = elements.filter(element => { return element.attributes.purpose === this.purposeFilter});
+ }
+ if (this.permissionFilter !== 'read') {
+ elements = elements.filter(element => { return element.attributes['can-edit'] });
}
- return this.children;
+
+ return elements;
},
colors() {
const colors = [
@@ -455,8 +530,17 @@ export default {
hasImage(child) {
return child.relationships?.image?.data !== null;
},
- getElementUrl(element_id) {
- return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/courseware#/structural_element/' + element_id;
+ getElementUrl(elementId) {
+ return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/courseware#/structural_element/' + elementId;
+ },
+ getSharedElementUrl(elementId) {
+ return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/shared_content_courseware/' + elementId;
+ },
+ getOwnerName(element) {
+ const ownerId = element.relationships.owner.data.id;
+ const owner = this.userById({ id: ownerId });
+
+ return owner.attributes['formatted-name'];
},
addElement() {
this.setShowOverviewElementAddDialog(true);
@@ -519,7 +603,7 @@ export default {
} else {
this.uploadFileError = '';
}
- },
+ }
},
watch: {
root(newRootObject) {
diff --git a/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue b/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue
index 3fd93ab..43fbd1e 100644
--- a/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue
+++ b/resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue
@@ -1,51 +1,99 @@
<template>
- <select v-model="purposeFilter" class="sidebar-selectlist">
- <option value="all">
- <translate>alle</translate>
- </option>
- <option value="content">
- <translate>Inhalt</translate>
- </option>
- <option 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>
+ <div class="cw-filter-widget">
+ <form class="default" @submit.prevent="">
+ <label>
+ <translate>Lernmaterialien</translate>
+ <select v-model="sourceFilter">
+ <option value="all">
+ <translate>Alle</translate>
+ </option>
+ <option value="personal">
+ <translate>Persönliche</translate>
+ </option>
+ <option value="shared">
+ <translate>Geteilte</translate>
+ </option>
+ </select>
+ </label>
+ <label>
+ <translate>Zweck</translate>
+ <select v-model="purposeFilter">
+ <option value="all">
+ <translate>Alle</translate>
+ </option>
+ <option value="content">
+ <translate>Inhalt</translate>
+ </option>
+ <option 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>
+ <label>
+ <translate>Rechte</translate>
+ <select v-model="permissionFilter">
+ <option value="read">
+ <translate>Lesen</translate>
+ </option>
+ <option value="write">
+ <translate>Lesen und schreiben</translate>
+ </option>
+ </select>
+ </label>
+ </form>
+ </div>
</template>
<script>
-import { mapActions, mapGetters } from 'vuex';
+import { mapActions } from 'vuex';
export default {
name: 'courseware-content-overview-filter-widget',
data() {
return {
- purposeFilter: 'all'
+ permissionFilter: 'read',
+ purposeFilter: 'all',
+ sourceFilter: 'all',
};
},
methods: {
...mapActions({
- setPurposeFilter: 'setPurposeFilter'
+ setPermissionFilter: 'setPermissionFilter',
+ setPurposeFilter: 'setPurposeFilter',
+ setSourceFilter: 'setSourceFilter',
}),
+ filterPermission() {
+ this.setPermissionFilter(this.permissionFilter);
+ },
filterPurpose() {
this.setPurposeFilter(this.purposeFilter);
- }
+ },
+ filterSource() {
+ this.setSourceFilter(this.sourceFilter);
+ },
},
watch: {
+ permissionFilter() {
+ this.filterPermission();
+ },
purposeFilter() {
this.filterPurpose();
- }
+ },
+ sourceFilter() {
+ this.filterSource();
+ },
}
}
-</script> \ No newline at end of file
+</script>
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 @@
+<template>
+ <div class="cw-element-permissions">
+ <studip-message-box v-if="message != false"
+ :type="message.type ? message.type : 'info'"
+ :details="message.details ? message.details : []"
+ :hideClose="false">
+ {{ $gettext(message.text) }}
+ </studip-message-box>
+ <table class="default">
+ <caption>
+ <translate>Personen</translate>
+ </caption>
+ <colgroup>
+ <col style="width:35%">
+ <col style="width:15%">
+ <col style="width:25%">
+ <col style="width:15%">
+ <col style="width:10%">
+ </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>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-if="listEmpty" class="empty">
+ <td colspan="5">
+ <translate>Es wurden noch keine Freigaben erteilt</translate>
+ </td>
+ </tr>
+ <tr v-for="(user_perm, index) of userPermsList" :key="index">
+ <td>
+ <label>
+ {{ user_perm['formatted-name'] }}
+ <i>{{ user_perm.username }}</i>
+ </label>
+ </td>
+ <td class="perm">
+ <input
+ class="right"
+ :title="$gettextInterpolate('Leserechte für %{ userName }', { userName: user_perm.username })"
+ type="radio"
+ :name="`${user_perm.id}_right`"
+ value="read"
+ :checked="userPermsList[index]['read'] && !userPermsList[index]['write']"
+ @change="updateReadWritePerm(index, $event.target.value)"
+ />
+ </td>
+ <td class="perm">
+ <input
+ class="right"
+ :title="$gettextInterpolate('Lese- und Schreibrechte für %{ userName }', { userName: user_perm.username })"
+ type="radio"
+ :name="`${user_perm.id}_right`"
+ value="write"
+ :checked="userPermsList[index]['read'] && userPermsList[index]['write']"
+ @change="updateReadWritePerm(index, $event.target.value)"
+ />
+ </td>
+ <td>
+ <input
+ style="cursor: pointer !important;"
+ :title="getExpiryTitle(user_perm.username, userPermsList[index]['expiry'])"
+ type="date"
+ :min="minDate"
+ :id="`${user_perm.id}_expiry`"
+ v-model="userPermsList[index]['expiry']"
+ @change="refreshReadWriteApproval"
+ />
+ </td>
+ <td class="actions">
+ <button
+ class="cw-permission-delete"
+ :title="$gettextInterpolate('Entfernen der Rechte von %{ userName }', { userName: user_perm.username })"
+ @click.prevent="confirmDeleteUserPerm(index)"
+ >
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <td colspan="5">
+ <span class="multibuttons">
+ <button class="button add cw-add-persons" @click.prevent="showAddMultiPersonDialog = true">
+ <translate>Personen hinzufügen</translate>
+ </button>
+ <button
+ class="button"
+ :class="{disabled: listEmpty}"
+ :disabled="listEmpty"
+ @click.prevent="setAllPerms('read')"
+ >
+ <translate>Allen Leserechte geben</translate>
+ </button>
+ <button
+ class="button"
+ :class="{disabled: listEmpty}"
+ :disabled="listEmpty"
+ @click.prevent="setAllPerms('write')"
+ >
+ <translate>Allen Lese- und Schreibrechte geben</translate>
+ </button>
+ </span>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ <studip-dialog
+ v-if="showAddMultiPersonDialog"
+ :title="$gettext('Personen hinzufügen')"
+ :confirmText="$gettext('Speichern')"
+ confirmClass="accept"
+ :closeText="$gettext('Schließen')"
+ closeClass="cancel"
+ @close="clearSelectedUsers"
+ @confirm="getSelectedUsers"
+ height="500"
+ width="750"
+ >
+ <template v-slot:dialogContent>
+ <studip-multi-person-search v-model="selectedUsers" name="content-persons"/>
+ </template>
+ </studip-dialog>
+ <studip-dialog
+ v-if="showDeleteDialog"
+ :title="$gettext('Personen löschen')"
+ :question="$gettext('Möchten Sie diese Person wirklich löschen?')"
+ height="180"
+ @confirm="performDeleteUserPerm"
+ @close="clearDeleteUserPerm"
+ ></studip-dialog>
+ </div>
+</template>
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+ name: 'courseware-content-permissions',
+ props: {
+ element: Object,
+ },
+ data() {
+ return {
+ showAddMultiPersonDialog: null,
+ userPermsList: [],
+ selectedUsers:[],
+ userPermsReadAll: false,
+ userPermsWriteAll: false,
+ userPermsReadUsers: [],
+ userPermsWriteUsers: [],
+ message: false,
+ showDeleteDialog: false,
+ deleteUserPermIndex: -1
+ };
+ },
+
+ 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();
+ },
+
+ computed: {
+ ...mapGetters({
+ userById: 'users/byId',
+ }),
+
+ listEmpty() {
+ return this.userPermsList.length === 0;
+ },
+
+ readApproval() {
+ return {
+ all: this.userPermsReadAll,
+ users: this.userPermsReadUsers,
+ groups: []
+ };
+ },
+
+ writeApproval() {
+ return {
+ all: this.userPermsWriteAll,
+ users: this.userPermsWriteUsers,
+ groups: []
+ };
+ },
+
+ minDate() {
+ let today = new Date();
+ return today.toISOString().split('T')[0];
+ }
+ },
+
+ methods: {
+ ...mapActions({
+ loadUser: 'users/loadById',
+ }),
+
+ getExpiryTitle(userName, date) {
+ if (date) {
+ return this.$gettextInterpolate('Die Berechtigungen für %{ userName } laufen am folgendem Datum ab: %{ dateStr }', { userName: userName, dateStr: new Date(date).toLocaleDateString() })
+ } else {
+ return this.$gettextInterpolate('Das Ablaufdatum der Berechtigungen für %{ userName }', { userName: userName });
+ }
+ },
+
+ async getUser(userId) {
+ await this.loadUser({id: userId});
+ const user = this.userById({id: userId});
+ return user;
+ },
+
+ 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;
+ }
+
+ for (const user_perm_obj of this.userPermsReadUsers) {
+ 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,
+ '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'],
+ });
+ }
+ },
+
+ async getSelectedUsers() {
+ this.message = false;
+ let duplicatedUsers = [];
+ if (this.selectedUsers.length) {
+ for (const selected_user of this.selectedUsers) {
+ let exists = this.userPermsList.some(user => {
+ return user.id === selected_user.id;
+ });
+ if (!exists) {
+ let newUserPerm = {
+ 'id': selected_user.id,
+ 'read': true,
+ 'write': false,
+ 'expiry': '',
+ 'formatted-name': selected_user['formatted-name'],
+ 'username': selected_user.username,
+ };
+ this.userPermsList.push(newUserPerm);
+ this.refreshReadWriteApproval();
+ } else {
+ duplicatedUsers.push(selected_user);
+ }
+ }
+ this.selectedUsers = [];
+ }
+ this.showAddMultiPersonDialog = false;
+
+ if (duplicatedUsers.length > 0) {
+ this.message = {};
+ this.message.text = this.$gettext('Die folgenden ausgewählten Personen existierten bereits:');
+ this.message.type = 'info';
+ this.message.details = [];
+ for (const duplicated of duplicatedUsers) {
+ this.message.details.push(duplicated['formatted-name']);
+ }
+ }
+ },
+
+ clearSelectedUsers() {
+ this.selectedUsers = [];
+ this.showAddMultiPersonDialog = false;
+ },
+
+ confirmDeleteUserPerm(index) {
+ this.deleteUserPermIndex = index;
+ this.showDeleteDialog = true;
+ },
+
+ performDeleteUserPerm() {
+ if (this.deleteUserPermIndex !== -1) {
+ this.userPermsList.splice(this.deleteUserPermIndex, 1);
+ this.refreshReadWriteApproval();
+ }
+ this.clearDeleteUserPerm();
+ },
+
+ clearDeleteUserPerm() {
+ this.deleteUserPermIndex = -1;
+ this.showDeleteDialog = false;
+ },
+
+ updateReadWritePerm(index, value) {
+ let read = false;
+ let write = false;
+
+ if (value === 'read') {
+ read = true;
+ } else if (value === 'write') {
+ read = true;
+ write = true;
+ }
+
+ this.userPermsList[index]['read'] = read;
+ this.userPermsList[index]['write'] = write;
+ this.refreshReadWriteApproval();
+ },
+
+ setAllPerms(permtype) {
+ if (this.listEmpty) {
+ return false;
+ }
+ let read = true;
+ let write = permtype === 'write';
+ this.userPermsList.every(item => {
+ item['read'] = read;
+ item['write'] = write;
+
+ return true;
+ });
+
+ this.refreshReadWriteApproval();
+ },
+
+ refreshReadWriteApproval() {
+ this.refreshReadApproval();
+ this.refreshWriteApproval();
+ },
+
+ refreshReadApproval() {
+ this.userPermsReadUsers = [];
+ for (const user_perm_obj of this.userPermsList) {
+ let readRight = user_perm_obj.write ? true : user_perm_obj.read;
+ this.userPermsReadUsers.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;
+ }
+ },
+ },
+};
+</script>
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 @@
+<template>
+ <div>
+ <table class="default">
+ <caption>
+ <translate>Von mir geteilte Lerninhalte</translate>
+ </caption>
+ <thead>
+ <tr>
+ <th><translate>Seite</translate></th>
+ <th><translate>Lesen</translate></th>
+ <th><translate>Lesen & Schreiben</translate></th>
+ <th class="actions"><translate>Aktionen</translate></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="element in releasedElements" :key="element.id">
+ <td>
+ <a :href="getElementUrl(element)">
+ {{ element.attributes.title }}
+ </a>
+ </td>
+ <td>
+ <span
+ v-if="element.attributes['read-approval'].users.length > 0"
+ role="checkbox"
+ aria-checked="true"
+ aria-disabled="true"
+ >
+ <studip-icon shape="accept" role="info" />
+ </span>
+ </td>
+ <td>
+ <span
+ v-if="element.attributes['write-approval'].users.length > 0"
+ role="checkbox"
+ aria-checked="true"
+ aria-disabled="true"
+ >
+ <studip-icon shape="accept" role="info" />
+ </span>
+ </td>
+ <td class="actions">
+ <studip-action-menu
+ :items="menuItems"
+ @editReleases="displayEditReleases(element)"
+ @clearReleases="displayClearReleases(element)"
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <studip-dialog
+ v-if="showEditReleases"
+ :title="$gettext('Freigabe bearbeiten')"
+ :confirmText="$gettext('Speichern')"
+ confirmClass="accept"
+ :closeText="$gettext('Schließen')"
+ closeClass="cancel"
+ height="480"
+ width="720"
+ @confirm="storeReleases"
+ @close="closeEditReleases"
+ >
+ <template v-slot:dialogContent>
+ <courseware-content-permissions
+ :element="selectedElement"
+ @updateReadApproval="updateReadApproval"
+ @updateWriteApproval="updateWriteApproval"
+ />
+ </template>
+ </studip-dialog>
+
+ <studip-dialog
+ v-if="showClearReleases"
+ :title="$gettext('Löschen der Freigabe')"
+ :question="$gettextInterpolate('Möchten Sie die Freigabe für %{ pageTitle} wirklich löschen?', {pageTitle: this.selectedElement.attributes.title})"
+ height="220"
+ @confirm="clearReleases"
+ @close="closeClearReleases"
+ ></studip-dialog>
+ </div>
+</template>
+
+<script>
+import CoursewareContentPermissions from './CoursewareContentPermissions.vue';
+import { mapActions, mapGetters } from 'vuex';
+import StudipActionMenu from './../StudipActionMenu.vue';
+import StudipDialog from '../StudipDialog.vue';
+import StudipIcon from '../StudipIcon.vue';
+
+export default {
+ name: 'courseware-content-shared',
+ components: {
+ CoursewareContentPermissions,
+ StudipActionMenu,
+ StudipDialog,
+ StudipIcon,
+ },
+ data() {
+ return {
+ menuItems: [
+ { id: 1, label: this.$gettext('Freigabe bearbeiten'), icon: 'edit', emit: 'editReleases' },
+ { id: 2, label: this.$gettext('Freigabe löschen'), icon: 'trash', emit: 'clearReleases' }
+ ],
+ showClearReleases: false,
+ showEditReleases: false,
+ selectedElement: null
+ }
+ },
+ computed: {
+ ...mapGetters({
+ releasedElements: 'courseware-structural-elements-released/all',
+ }),
+ },
+ methods: {
+ ...mapActions({
+ updateStructuralElement: 'updateStructuralElement',
+ lockObject: 'lockObject',
+ unlockObject: 'unlockObject',
+ relaodSharedElements: 'courseware-structural-elements-released/loadAll'
+ }),
+ 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;
+ },
+ displayEditReleases(element) {
+ this.selectedElement = element;
+ this.showEditReleases = true;
+ },
+ async storeReleases() {
+ const currentId = this.selectedElement.id;
+ await this.lockObject({ id: currentId, type: 'courseware-structural-elements' });
+ await this.updateStructuralElement({
+ element: this.selectedElement,
+ id: currentId,
+ });
+ await this.unlockObject({ id: currentId, type: 'courseware-structural-elements' });
+ this.closeEditReleases();
+ },
+ closeEditReleases() {
+ this.showEditReleases = false;
+ this.selectedElement = null;
+ },
+ displayClearReleases(element) {
+ this.selectedElement = element;
+ this.showClearReleases = true;
+ },
+ async clearReleases() {
+ const currentId = this.selectedElement.id;
+ this.selectedElement.attributes['read-approval'].users = [];
+ this.selectedElement.attributes['write-approval'].users = [];
+ await this.lockObject({ id: currentId, type: 'courseware-structural-elements' });
+ await this.updateStructuralElement({
+ element: this.selectedElement,
+ id: currentId,
+ });
+ await this.unlockObject({ id: currentId, type: 'courseware-structural-elements' });
+ this.closeClearReleases();
+ this.relaodSharedElements();
+ },
+ closeClearReleases() {
+ this.showClearReleases = false;
+ this.selectedElement = null;
+ },
+ },
+}
+</script> \ 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"
/>
+ <courseware-content-permissions
+ v-if="inContent"
+ :element="currentElement"
+ @updateReadApproval="updateReadApproval"
+ @updateWriteApproval="updateWriteApproval"
+ />
</courseware-tab>
<courseware-tab v-if="inCourse" :name="textEdit.visible" :index="4">
<form class="default" @submit.prevent="">
@@ -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;
},