diff options
| author | Murtaza Sultani <sultani@data-quest.de> | 2025-09-23 09:03:58 +0200 |
|---|---|---|
| committer | Murtaza Sultani <sultani@data-quest.de> | 2025-09-23 09:03:58 +0200 |
| commit | 349521e801bea9a07f1b8ec1b3261cf9077e3788 (patch) | |
| tree | 2bafc06a104520099df6bcdcde55717388b259ed | |
| parent | 2d261197c18af19c78ea66a88877da84692c02a9 (diff) | |
Resolve "Forum: Bearbeitungs und Löschrechte für Dozenten und Tutoren hinzufügen"
Closes #5757
Merge request studip/studip!4393
| -rw-r--r-- | lib/classes/JsonApi/Routes/Forum/Authority.php | 11 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Forum/ConfigIndex.php | 1 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Forum/PostingDelete.php | 16 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Forum/PostingUpdate.php | 17 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Schemas/Forum/Posting.php | 11 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Schemas/Forum/Topic.php | 1 | ||||
| -rw-r--r-- | resources/vue/components/forum/ForumApp.vue | 5 | ||||
| -rw-r--r-- | resources/vue/components/forum/posts/Post.vue | 95 | ||||
| -rw-r--r-- | resources/vue/store/pinia/forum/ForumConfig.js | 2 |
9 files changed, 88 insertions, 71 deletions
diff --git a/lib/classes/JsonApi/Routes/Forum/Authority.php b/lib/classes/JsonApi/Routes/Forum/Authority.php index 95091a0..2ad779c 100644 --- a/lib/classes/JsonApi/Routes/Forum/Authority.php +++ b/lib/classes/JsonApi/Routes/Forum/Authority.php @@ -1,6 +1,7 @@ <?php namespace JsonApi\Routes\Forum; +use Forum\Posting; use Range; use User; @@ -10,4 +11,14 @@ class Authority { return $range->isAccessibleToUser($user?->user_id); } + + public static function canEditPost(User $user, Posting $posting, $isDiscussionClosed = false): bool + { + return (!$isDiscussionClosed && $posting->user_id === $user->user_id) || $GLOBALS['perm']->have_studip_perm('tutor', $posting->range_id, $user->id); + } + + public static function canDeletePost(User $user, Posting $posting, $isDiscussionClosed = false): bool + { + return self::canEditPost($user, $posting, $isDiscussionClosed); + } } diff --git a/lib/classes/JsonApi/Routes/Forum/ConfigIndex.php b/lib/classes/JsonApi/Routes/Forum/ConfigIndex.php index 8383608..de9a9a6 100644 --- a/lib/classes/JsonApi/Routes/Forum/ConfigIndex.php +++ b/lib/classes/JsonApi/Routes/Forum/ConfigIndex.php @@ -28,6 +28,7 @@ class ConfigIndex extends JsonApiController return $this->getMetaResponse([ 'is-admin' => CoreForum::isAdmin($range->id), 'is-moderator' => CoreForum::isModerator($range->id), + 'is-tutor' => $GLOBALS['perm']->have_studip_perm('tutor', $range->id, $user->id), 'anonymous-post' => (bool) Config::get()->FORUM_ANONYMOUS_POSTINGS, 'tile-layout' => (bool) UserConfig::get($user->user_id)->FORUM_TILE_LAYOUT ]); diff --git a/lib/classes/JsonApi/Routes/Forum/PostingDelete.php b/lib/classes/JsonApi/Routes/Forum/PostingDelete.php index e49755d..64225b3 100644 --- a/lib/classes/JsonApi/Routes/Forum/PostingDelete.php +++ b/lib/classes/JsonApi/Routes/Forum/PostingDelete.php @@ -3,7 +3,6 @@ namespace JsonApi\Routes\Forum; use JsonApi\Errors\AuthorizationFailedException; use JsonApi\Errors\RecordNotFoundException; -use JsonApi\Routes\Courses\Authority as CourseAuthority; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use JsonApi\JsonApiController; @@ -13,21 +12,14 @@ class PostingDelete extends JsonApiController { public function __invoke(Request $request, Response $response, $args) { - $user = $this->getUser($request); - - $posting = Posting::findOneBySQL( - "posting_id = :posting_id AND user_id = :user_id", - [ - 'posting_id' => $args['posting_id'], - 'user_id' => $user->user_id - ] - ); - + $posting = Posting::find($args['posting_id']); if (!$posting) { throw new RecordNotFoundException(); } - if ($posting->discussion->closed_at) { + if ( + !Authority::canDeletePost($this->getUser($request), $posting, (bool) $posting->discussion->closed_at) + ) { throw new AuthorizationFailedException(); } diff --git a/lib/classes/JsonApi/Routes/Forum/PostingUpdate.php b/lib/classes/JsonApi/Routes/Forum/PostingUpdate.php index cd50373..63b1ce5 100644 --- a/lib/classes/JsonApi/Routes/Forum/PostingUpdate.php +++ b/lib/classes/JsonApi/Routes/Forum/PostingUpdate.php @@ -25,25 +25,18 @@ class PostingUpdate extends JsonApiController public function __invoke(Request $request, Response $response, $args) { - $json = $this->validate($request); - $user = $this->getUser($request); - - $posting = Posting::findOneBySQL( - "posting_id = :posting_id AND user_id = :user_id", - [ - 'posting_id' => $args['posting_id'], - 'user_id' => $user->user_id - ] - ); - + $posting = Posting::find($args['posting_id']); if (!$posting) { throw new RecordNotFoundException(); } - if ($posting->discussion->closed_at) { + if ( + !Authority::canEditPost($this->getUser($request), $posting, (bool) $posting->discussion->closed_at) + ) { throw new AuthorizationFailedException(); } + $json = $this->validate($request); $posting->content = Markup::purifyHtml(Markup::markAsHtml(self::arrayGet($json, 'data.attributes.content'))); $posting->anonymous = (self::arrayGet($json, 'data.attributes.anonymous') && \Config::get()->FORUM_ANONYMOUS_POSTINGS); $posting->store(); diff --git a/lib/classes/JsonApi/Schemas/Forum/Posting.php b/lib/classes/JsonApi/Schemas/Forum/Posting.php index aa8423a..e2323ec 100644 --- a/lib/classes/JsonApi/Schemas/Forum/Posting.php +++ b/lib/classes/JsonApi/Schemas/Forum/Posting.php @@ -26,7 +26,6 @@ class Posting extends SchemaProvider } /** - * @inheritDoc * @param \Forum\Posting $resource */ public function getAttributes($resource, ContextInterface $context): iterable @@ -49,7 +48,6 @@ class Posting extends SchemaProvider } /** - * @inheritDoc * @param \Forum\Posting $resource */ public function getResourceMeta($resource) @@ -79,7 +77,7 @@ class Posting extends SchemaProvider return $relationships; } - private function addAuthorRelationship(array $relationships, \Forum\Posting $posting, $withAuthor = false) + private function addAuthorRelationship(array $relationships, \Forum\Posting $posting, bool $withAuthor = false) { $author = $posting->author; @@ -95,7 +93,7 @@ class Posting extends SchemaProvider return $relationships; } - private function addDiscussionRelationship(array $relationships, \Forum\Posting $posting, $withDiscussion = false) + private function addDiscussionRelationship(array $relationships, \Forum\Posting $posting, bool $withDiscussion = false) { if ($withDiscussion) { $relationships[self::REL_DISCUSSION] = [ @@ -109,7 +107,7 @@ class Posting extends SchemaProvider return $relationships; } - private function addPostingRelationship(array $relationships, \Forum\Posting $posting, $withPosting = false) + private function addPostingRelationship(array $relationships, \Forum\Posting $posting, bool $withPosting = false) { $posting = $posting->posting; @@ -125,7 +123,7 @@ class Posting extends SchemaProvider return $relationships; } - private function addReactionsRelationship(array $relationships, \Forum\Posting $posting, $withReactions = false) + private function addReactionsRelationship(array $relationships, \Forum\Posting $posting, bool $withReactions = false) { if ($withReactions) { $relationships[self::REL_REACTIONS] = [ @@ -138,4 +136,5 @@ class Posting extends SchemaProvider return $relationships; } + } diff --git a/lib/classes/JsonApi/Schemas/Forum/Topic.php b/lib/classes/JsonApi/Schemas/Forum/Topic.php index 06022c6..22a8c83 100644 --- a/lib/classes/JsonApi/Schemas/Forum/Topic.php +++ b/lib/classes/JsonApi/Schemas/Forum/Topic.php @@ -21,7 +21,6 @@ class Topic extends SchemaProvider } /** - * @inheritdoc * @param \Forum\Topic $resource */ public function getAttributes($resource, ContextInterface $context): iterable diff --git a/resources/vue/components/forum/ForumApp.vue b/resources/vue/components/forum/ForumApp.vue index dc0adc0..7a0ea35 100644 --- a/resources/vue/components/forum/ForumApp.vue +++ b/resources/vue/components/forum/ForumApp.vue @@ -10,8 +10,9 @@ const fetchConfigs = async () => { forumConfig.$patch({ isModerator: response.meta['is-moderator'], isAdmin: response.meta['is-admin'], + isTutor: response.meta['is-tutor'], anonymousPost: response.meta['anonymous-post'], - tileLayout: response.meta['tile-layout'], + tileLayout: response.meta['tile-layout'] }); } catch (error) { STUDIP.Report.error(error); @@ -26,7 +27,7 @@ onMounted(async () => { } else { await fetchConfigs(); } -}) +}); </script> <template> diff --git a/resources/vue/components/forum/posts/Post.vue b/resources/vue/components/forum/posts/Post.vue index ed27d6e..1c7e302 100644 --- a/resources/vue/components/forum/posts/Post.vue +++ b/resources/vue/components/forum/posts/Post.vue @@ -39,11 +39,12 @@ const postContent = useTemplateRef('postContent'); const userAvatarContainer = useTemplateRef('userAvatarContainer'); const selectedText = ref(''); -const editPost = ref(''); -const postCreateForm = ref(false); +const showPostEditForm = ref(false); +const showPostCreateForm = ref(false); const isUnread = computed(() => (!props.post.author && props.is_unread) || (props.is_unread && props.post.author.id !== STUDIP.USER_ID)) - +const canEditPost = computed(() => forumConfig.isTutor || (props.post.author?.id === STUDIP.USER_ID && !props.discussion.closed_at)); +const canDeletePost = computed(() => canEditPost.value); const copyToClipboard = () => { if (selectedText.value) { navigator.clipboard.writeText(selectedText.value); @@ -52,13 +53,25 @@ const copyToClipboard = () => { } } -const deletePost = async (post) => { +const editPost = () => { + if (!canEditPost.value) { + return; + } + + showPostEditForm.value = true; +} + +const deletePost = async () => { + if (!canDeletePost.value) { + return; + } + STUDIP.Dialog.confirm( $gettext('Wollen Sie diesen Beitrag löschen?'), async () => { try { - await STUDIP.jsonapi.withPromises().DELETE(`forum-postings/${post.id}`); - forumDiscussionPost.removePost(post.id); + await STUDIP.jsonapi.withPromises().DELETE(`forum-postings/${props.post.id}`); + forumDiscussionPost.removePost(props.post.id); STUDIP.Report.success($gettext('Der Beitrag wurde gelöscht.')); } catch (error) { STUDIP.Report.error(error); @@ -69,11 +82,11 @@ const deletePost = async (post) => { const addPost = () => { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); - postCreateForm.value = false; + showPostCreateForm.value = false; } const addReply = post => { - postCreateForm.value = true; + showPostCreateForm.value = true; selectedText.value = post.content; } @@ -98,12 +111,12 @@ const forwardPost = post => { } const removePostHighlight = id => { - const element = document.getElementById(id) + const element = document.getElementById(id); if (!element) { - console.error("Element not found!") - return + console.error("Element not found!"); + return; } - element.classList.remove('--highlight') + element.classList.remove('--highlight'); } </script> @@ -170,8 +183,8 @@ const removePostHighlight = id => { </span> <StudipDateTime v-else :iso="post.mkdate" :relative="true" /> </div> - <template v-if="editPost === post.id"> - <PostEditForm :post="post" :auth_user="auth_user" class="mt-10" @canceled="editPost = ''" @updated="editPost = ''"/> + <template v-if="showPostEditForm"> + <PostEditForm :post="post" :auth_user="auth_user" class="mt-10" @canceled="showPostEditForm = false" @updated="showPostEditForm = false"/> </template> <template v-else> <div class="post__text"> @@ -180,8 +193,8 @@ const removePostHighlight = id => { <a :href="`#create_form_${post.id}`" class="ballon-action__button" - v-if="!forumConfig.allowGuestAccess && !postCreateForm && !discussion.closed_at" - @click="postCreateForm = true; postContent.removeSelection()" + v-if="!forumConfig.allowGuestAccess && !showPostCreateForm && !discussion.closed_at" + @click="showPostCreateForm = true; postContent.removeSelection()" :title="$gettext('Auswahl zitieren und antworten')" :aria-label="$gettext('Auswahl zitieren und antworten')" > @@ -210,35 +223,41 @@ const removePostHighlight = id => { <div class="post__footer"> <div></div> <div class="inline-flex items-center gap-40"> - <div v-if="!forumConfig.allowGuestAccess && !discussion.closed_at" class="inline-flex items-center gap-10"> - <template v-if="post.author?.id === auth_user.id"> - <a - :href="`#post_${post.id}`" - @click="editPost = post.id" - type="button" - class="button button--icon-only" - :class="{ - 'disabled': editPost === post.id - }" - :title="$gettext('Beitrag bearbeiten')" - :aria-label="$gettext('Beitrag bearbeiten')" - > - <StudipIcon shape="edit" :size="20" aria-hidden="true" /> - </a> - <button @click="deletePost(post)" type="button" class="button button--icon-only" :title="$gettext('Beitrag löschen')" :aria-label="$gettext('Beitrag löschen')"> - <StudipIcon shape="trash" :size="20" aria-hidden="true" /> - </button> - </template> + <div v-if="!forumConfig.allowGuestAccess" class="inline-flex items-center gap-10"> + <a + v-if="canEditPost" + :href="`#post_${post.id}`" + @click="editPost" + type="button" + class="button button--icon-only" + :class="{ + 'disabled': showPostEditForm + }" + :title="$gettext('Beitrag bearbeiten')" + :aria-label="$gettext('Beitrag bearbeiten')" + > + <StudipIcon shape="edit" :size="20" aria-hidden="true" /> + </a> + <button + v-if="canDeletePost" + @click="deletePost" + type="button" class="button button--icon-only" + :title="$gettext('Beitrag löschen')" + :aria-label="$gettext('Beitrag löschen')" + > + <StudipIcon shape="trash" :size="20" aria-hidden="true" /> + </button> <button type="button" @click="forwardPost(post)" class="button button--icon-only" :title="$gettext('Beitrag weiterleiten')" :aria-label="$gettext('Beitrag weiterleiten')"> <StudipIcon shape="export" :size="20" aria-hidden="true" /> </button> <a + v-if="!discussion.closed_at" :href="`#create_form_${post.id}`" @click="addReply(post)" type="button" class="button button--icon-only" :class="{ - 'disabled': postCreateForm + 'disabled': showPostCreateForm }" :title="$gettext('Zitieren und antworten')" :aria-label="$gettext('Zitieren und Antworten')" @@ -251,13 +270,13 @@ const removePostHighlight = id => { </div> </div> </div> - <div v-if="postCreateForm && !discussion.closed_at" :id="`create_form_${post.id}`" class="post-form-container" style="scroll-margin-top: 200px;"> + <div v-if="showPostCreateForm && !discussion.closed_at" :id="`create_form_${post.id}`" class="post-form-container" style="scroll-margin-top: 200px;"> <PostCreateForm :parent_id="post.id" :discussion_id="props.discussion.discussion_id" :auth_user="auth_user" v-model:quote="selectedText" - @canceled="postCreateForm = false; selectedText = ''" + @canceled="showPostCreateForm = false; selectedText = ''" @created="addPost" /> </div> diff --git a/resources/vue/store/pinia/forum/ForumConfig.js b/resources/vue/store/pinia/forum/ForumConfig.js index 26b008c..16b028c 100644 --- a/resources/vue/store/pinia/forum/ForumConfig.js +++ b/resources/vue/store/pinia/forum/ForumConfig.js @@ -7,6 +7,7 @@ export const useForumConfig = defineStore( const allowGuestAccess = ref(false); const isAdmin = ref(false); const isModerator = ref(false); + const isTutor = ref(false); const anonymousPost = ref(false); const tileLayout = ref(true); @@ -32,6 +33,7 @@ export const useForumConfig = defineStore( isModerator, anonymousPost, tileLayout, + isTutor, toggleForumLayout } } |
