aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMurtaza Sultani <sultani@data-quest.de>2025-09-23 09:03:58 +0200
committerMurtaza Sultani <sultani@data-quest.de>2025-09-23 09:03:58 +0200
commit349521e801bea9a07f1b8ec1b3261cf9077e3788 (patch)
tree2bafc06a104520099df6bcdcde55717388b259ed
parent2d261197c18af19c78ea66a88877da84692c02a9 (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.php11
-rw-r--r--lib/classes/JsonApi/Routes/Forum/ConfigIndex.php1
-rw-r--r--lib/classes/JsonApi/Routes/Forum/PostingDelete.php16
-rw-r--r--lib/classes/JsonApi/Routes/Forum/PostingUpdate.php17
-rw-r--r--lib/classes/JsonApi/Schemas/Forum/Posting.php11
-rw-r--r--lib/classes/JsonApi/Schemas/Forum/Topic.php1
-rw-r--r--resources/vue/components/forum/ForumApp.vue5
-rw-r--r--resources/vue/components/forum/posts/Post.vue95
-rw-r--r--resources/vue/store/pinia/forum/ForumConfig.js2
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
}
}