From d83a8347ed60b06b360827dc8a1026a70815a483 Mon Sep 17 00:00:00 2001 From: Murtaza Sultani Date: Fri, 25 Jul 2025 12:25:37 +0200 Subject: Resolve "Forumsuche ohne Reload" Closes #5747 Merge request studip/studip!4388 --- app/controllers/course/forum/discussions.php | 2 +- app/controllers/course/forum/search.php | 185 +++++---------------- app/controllers/course/forum/topics.php | 2 +- .../JsonApi/Routes/Forum/ForumDiscussionIndex.php | 65 +++++++- lib/models/Forum/ForumDiscussion.php | 54 +++++- resources/vue/apps/forum/discussions/Edit.vue | 2 +- resources/vue/apps/forum/search/Index.vue | 142 ++++++++++------ .../components/forum/topics/SelectTopicInput.vue | 2 +- 8 files changed, 251 insertions(+), 203 deletions(-) diff --git a/app/controllers/course/forum/discussions.php b/app/controllers/course/forum/discussions.php index 7e7aca7..51f2c0f 100644 --- a/app/controllers/course/forum/discussions.php +++ b/app/controllers/course/forum/discussions.php @@ -105,7 +105,7 @@ class Course_Forum_DiscussionsController extends Forum\ForumBaseController 'category' => $category ? $category->toRawArray() : [], 'read_index' => (int) ($posting_read ? $posting_read->read_index : 0), 'redirect' => Request::option('redirect'), - 'search_keyword' => $_SESSION['forum'][$this->range_id]['search']['keyword'] ?? '' + 'search_keyword' => $_SESSION['forum'][$this->range_id]['search_filter']['keyword'] ?? '' ]) ); } diff --git a/app/controllers/course/forum/search.php b/app/controllers/course/forum/search.php index c9f4588..1a028c1 100644 --- a/app/controllers/course/forum/search.php +++ b/app/controllers/course/forum/search.php @@ -1,9 +1,7 @@ buildSearchObject(); - $all_tags = array_map(fn(ForumTag $tag) => $tag->toRawArray(), ForumTag::getForumTags()); + $tags = array_map(fn(ForumTag $tag) => $tag->toRawArray(), ForumTag::getForumTags()); $discussion_types = array_map(fn(ForumDiscussionType $discussion_type) => $discussion_type->toRawArray(), ForumDiscussionType::getForumDiscussionType()); $this->render_vue_app( Studip\VueApp::create('forum/search/Index') ->withProps([ - 'search' => $search_object, - 'discussions' => $this->getResult($search_object), + 'filter' => $this->getForumFilter(), 'topics' => $topics, 'discussion_types' => $discussion_types, - 'tags' => $all_tags, + 'tags' => $tags, 'course_members' => $course_members, ]) ); } - private function getResult($search_object): array + private function getForumFilter(): array { - if ($this->isSearchObjectEmpty($search_object)) { - unset($_SESSION['forum'][$this->range_id]['search']); - return []; - } + $request = Request::getInstance(); + $filter = []; + $session_filter = $_SESSION['forum'][$this->range_id]['search_filter'] ?? []; - $query = [ - "SELECT - discussions.*, - MAX(postings.mkdate) AS latest_post_date - FROM forum_discussions AS discussions - LEFT JOIN forum_postings AS postings USING(discussion_id) - LEFT JOIN tags_relations ON (tags_relations.range_id = discussions.discussion_id AND range_type = 'forum') - WHERE postings.range_id = :range_id ", - [ - 'range_id' => $this->range_id - ] - ]; - - $keyword = $search_object['keyword']; - if ($keyword) { - $query[0] .= " AND (discussions.title LIKE :keyword OR postings.content LIKE :keyword)"; - $query[1]["keyword"] = "%$keyword%"; + if ($request->offsetExists('keyword')) { + $filter['keyword'] = Request::get('keyword'); + } else if (isset($session_filter['keyword'])) { + $filter['keyword'] = $session_filter['keyword']; } - if ($search_object['begin']) { - $query[0] .= " AND postings.mkdate >= :begin"; - $query[1]['begin'] = $search_object['begin']; + if ($request->offsetExists('begin')) { + $filter['begin'] = Request::int('begin'); + } else if (isset($session_filter['begin'])) { + $filter['begin'] = (int) $session_filter['begin']; } - if ($search_object['end']) { - $query[0] .= " AND postings.mkdate <= :end"; - $query[1]['end'] = $search_object['end']; + if ($request->offsetExists('end')) { + $filter['end'] = Request::int('end'); + } else if (isset($session_filter['end'])) { + $filter['end'] = (int) $session_filter['end']; } - if ($search_object['topic_ids']) { - $query[0] .= " AND discussions.topic_id IN (:topic_ids)"; - $query[1]['topic_ids'] = $search_object['topic_ids']; + if ($request->offsetExists('status')) { + $filter['status'] = Request::int('status'); + } else if (isset($session_filter['status'])) { + $filter['status'] = (int) $session_filter['status']; } - if ($search_object['discussion_type_ids']) { - $query[0] .= " AND discussions.type_id IN (:type_ids)"; - $query[1]['type_ids'] = $search_object['discussion_type_ids']; + if ($request->offsetExists('type_ids')) { + $filter['type_ids'] = Request::getArray('type_ids'); + } else if (isset($session_filter['type_ids'])) { + $filter['type_ids'] = $session_filter['type_ids']; } - if ($search_object['tag_ids']) { - $query[0] .= " AND tags_relations.tag_id IN (:tag_ids)"; - $query[1]['tag_ids'] = $search_object['tag_ids']; + if ($request->offsetExists('tag_ids')) { + $filter['tag_ids'] = Request::getArray('tag_ids'); + } else if (isset($session_filter['tag_ids'])) { + $filter['tag_ids'] = $session_filter['tag_ids']; } - if ($search_object['user_ids']) { - $query[0] .= " AND postings.user_id IN (:user_ids)"; - $query[1]['user_ids'] = $search_object['user_ids']; + if ($request->offsetExists('topic_ids')) { + $filter['topic_ids'] = Request::getArray('topic_ids'); + } else if (isset($session_filter['topic_ids'])) { + $filter['topic_ids'] = $session_filter['topic_ids']; } - $query[0] .= match ($search_object['discussion_status']) { - 2 => " AND discussions.closed_at IS NULL", // opens - 3 => " AND discussions.closed_at IS NOT NULL", // closed - default => "" - }; - - $discussions = DBManager::get()->fetchAll( - $query[0]." GROUP BY discussions.discussion_id ORDER BY latest_post_date DESC", - $query[1], - ForumDiscussion::buildExisting(...) - ); - - return array_map(function (ForumDiscussion $discussion) { - $members = array_map(fn(ForumMember $member) => $member->toRawArray(), $discussion->members); - $tags = array_map(fn(ForumTag $tag) => $tag->toRawArray(), $discussion->tags); - $metadata = $discussion->getMetaData(); - - return [ - 'id' => $discussion->discussion_id, - 'title' => $discussion->title, - 'closed_at' => $discussion->closed_at ? date('c', $discussion->closed_at) : null, - 'view_count' => (int) $discussion->view_count, - 'sticky' => (bool) $discussion->sticky, - 'mkdate' => date('c', $discussion->mkdate), - 'chdate' => date('c', $discussion->chdate), - 'topic' => $discussion->topic->toRawArray(), - 'category' => $discussion->category ? [ - 'name' => $discussion->category->name, - 'color' => $discussion->category->color, - ] : [], - 'discussion_type' => $discussion->discussion_type ? [ - 'name' => $discussion->discussion_type->name, - 'icon' => $discussion->discussion_type->icon, - ] : [], - 'members' => $members, - 'tags' => $tags, - 'meta' => [ - 'postings_count' => (int) $metadata['postings_count'], - 'recent_activity' => $metadata['recent_activity'] ? date('c', $metadata['recent_activity']) : null, - ] - ]; - }, $discussions); - } - - private function isSearchObjectEmpty($search_object): bool { - if ( - $search_object['keyword'] || - $search_object['begin'] || - $search_object['end'] || - $search_object['discussion_status'] || - $search_object['discussion_type_ids'] || - $search_object['tag_ids'] || - $search_object['topic_ids'] || - $search_object['user_ids'] - ) { - return false; - } - - return true; - } - - private function buildSearchObject(): array - { - $request = Request::getInstance(); - if ( - $request->offsetExists('keyword') || - $request->offsetExists('begin') || - $request->offsetExists('end') || - $request->offsetExists('discussion_status') || - $request->offsetExists('discussion_type_ids') || - $request->offsetExists('tag_ids') || - $request->offsetExists('topic_ids') || - $request->offsetExists('user_ids') - ) { - $search_object = [ - 'keyword' => Request::get('keyword'), - 'begin' => Request::int('begin'), - 'end' => Request::int('end'), - 'discussion_status' => Request::int('discussion_status'), - 'discussion_type_ids' => Request::getArray('discussion_type_ids'), - 'tag_ids' => Request::getArray('tag_ids'), - 'topic_ids' => Request::getArray('topic_ids'), - 'user_ids' => Request::getArray('user_ids') - ]; - - $_SESSION['forum'][$this->range_id]['search'] = $search_object; - return $search_object; + if ($request->offsetExists('user_ids')) { + $filter['user_ids'] = Request::getArray('user_ids'); + } else if (isset($session_filter['user_ids'])) { + $filter['user_ids'] = $session_filter['user_ids']; } - $session_search = $_SESSION['forum'][$this->range_id]['search'] ?? []; - return [ - 'keyword' => $session_search['keyword'] ?? '', - 'begin' => $session_search['begin'] ?? 0, - 'end' => $session_search['end'] ?? 0, - 'discussion_status' => $session_search['discussion_status'] ?? 0, - 'discussion_type_ids' => $session_search['discussion_type_ids'] ?? [], - 'tag_ids' => $session_search['tag_ids'] ?? [], - 'topic_ids' => $session_search['topic_ids'] ?? [], - 'user_ids' => $session_search['user_ids'] ?? [] - ]; + return $filter; } } diff --git a/app/controllers/course/forum/topics.php b/app/controllers/course/forum/topics.php index 652ecf9..a72da84 100644 --- a/app/controllers/course/forum/topics.php +++ b/app/controllers/course/forum/topics.php @@ -11,7 +11,7 @@ class Course_Forum_TopicsController extends Forum\ForumBaseController { parent::before_filter($action, $args); - unset($_SESSION['forum'][$this->range_id]['search']); + unset($_SESSION['forum'][$this->range_id]['search_filter']); Navigation::activateItem('course/forum/topics'); } diff --git a/lib/classes/JsonApi/Routes/Forum/ForumDiscussionIndex.php b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionIndex.php index 140a502..83af8ac 100644 --- a/lib/classes/JsonApi/Routes/Forum/ForumDiscussionIndex.php +++ b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionIndex.php @@ -12,7 +12,17 @@ use Forum\ForumDiscussion; class ForumDiscussionIndex extends JsonApiController { protected $allowedPagingParameters = ['offset', 'limit']; - protected $allowedFilteringParameters = ['last-visit']; + protected $allowedFilteringParameters = [ + 'last-visit', + 'keyword', + 'begin', + 'end', + 'topic-ids', + 'type-ids', + 'tag-ids', + 'user-ids', + 'status' + ]; protected $allowedIncludePaths = [ \JsonApi\Schemas\Forum\ForumCategory::REL_TOPICS, \JsonApi\Schemas\Forum\ForumDiscussion::REL_CATEGORY, @@ -35,14 +45,61 @@ class ForumDiscussionIndex extends JsonApiController throw new AuthorizationFailedException(); } - $filtering = $this->getQueryParameters()->getFilteringParameters() ?: []; - $last_visit = $filtering['last-visit'] ?? 0; + $filters = $this->getFilter(); + if ($filters) { + $_SESSION['forum'][$range->id]['search_filter'] = $filters; + } - $discussions = ForumDiscussion::getCourseDiscussions($range->id, $last_visit); + $discussions = ForumDiscussion::getCourseDiscussions($range->id, $filters); return $this->getPaginatedContentResponse( array_slice($discussions, ...$this->getOffsetAndLimit()), count($discussions) ); } + + private function getFilter(): array + { + $filtering = $this->getQueryParameters()->getFilteringParameters() ?: []; + + $discussion_filter = []; + + if (isset($filtering['last-visit'])) { + $discussion_filter['last_visit'] = (int) $filtering['last-visit']; + } + + if (isset($filtering['keyword'])) { + $discussion_filter['keyword'] = $filtering['keyword']; + } + + if (isset($filtering['status'])) { + $discussion_filter['status'] = (int) $filtering['status']; + } + + if (isset($filtering['begin'])) { + $discussion_filter['begin'] = (int) $filtering['begin']; + } + + if (isset($filtering['end'])) { + $discussion_filter['end'] = (int) $filtering['end']; + } + + if (isset($filtering['topic-ids'])) { + $discussion_filter['topic_ids'] = explode(',', $filtering['topic-ids']); + } + + if (isset($filtering['type-ids'])) { + $discussion_filter['type_ids'] = explode(',', $filtering['type-ids']); + } + + if (isset($filtering['tag-ids'])) { + $discussion_filter['tag_ids'] = explode(',', $filtering['tag-ids']); + } + + if (isset($filtering['user-ids'])) { + $discussion_filter['user_ids'] = explode(',', $filtering['user-ids']); + } + + return $discussion_filter; + } } diff --git a/lib/models/Forum/ForumDiscussion.php b/lib/models/Forum/ForumDiscussion.php index ce648fa..0dae82e 100644 --- a/lib/models/Forum/ForumDiscussion.php +++ b/lib/models/Forum/ForumDiscussion.php @@ -80,9 +80,12 @@ class ForumDiscussion extends SimpleORMap } /** + * @param string $range_id course_id or institute_id. + * @param array $filter Optional: filters to apply. + * * @return self[] */ - public static function getCourseDiscussions($range_id, $last_visit = 0): array + public static function getCourseDiscussions(string $range_id, array $filter = []): array { $query = [ "SELECT @@ -91,13 +94,58 @@ class ForumDiscussion extends SimpleORMap FROM forum_discussions AS discussions JOIN forum_postings as postings USING (discussion_id) JOIN forum_topics AS topics USING (topic_id) + LEFT JOIN tags_relations ON (tags_relations.range_id = discussions.discussion_id AND range_type = 'forum') WHERE topics.range_id = :range_id", ['range_id' => $range_id] ]; - if ($last_visit) { + if (isset($filter['last_visit'])) { $query[0] .= " AND postings.mkdate > :last_visit"; - $query[1]["last_visit"] = $last_visit; + $query[1]["last_visit"] = $filter['last_visit']; + } + + if (isset($filter['keyword'])) { + $keyword = $filter['keyword']; + $query[0] .= " AND (discussions.title LIKE :keyword OR postings.content LIKE :keyword)"; + $query[1]["keyword"] = "%$keyword%"; + } + + if (isset($filter['begin'])) { + $query[0] .= " AND postings.mkdate >= :begin"; + $query[1]['begin'] = $filter['begin']; + } + + if (isset($filter['end'])) { + $query[0] .= " AND postings.mkdate <= :end"; + $query[1]['end'] = $filter['end']; + } + + if (isset($filter['topic_ids'])) { + $query[0] .= " AND discussions.topic_id IN (:topic_ids)"; + $query[1]['topic_ids'] = $filter['topic_ids']; + } + + if (isset($filter['type_ids'])) { + $query[0] .= " AND discussions.type_id IN (:type_ids)"; + $query[1]['type_ids'] = $filter['type_ids']; + } + + if (isset($filter['tag_ids'])) { + $query[0] .= " AND tags_relations.tag_id IN (:tag_ids)"; + $query[1]['tag_ids'] = $filter['tag_ids']; + } + + if (isset($filter['user_ids'])) { + $query[0] .= " AND postings.user_id IN (:user_ids)"; + $query[1]['user_ids'] = $filter['user_ids']; + } + + if (isset($filter['status'])) { + $query[0] .= match ($filter['status']) { + 2 => " AND discussions.closed_at IS NULL", // opens + 3 => " AND discussions.closed_at IS NOT NULL", // closed + default => "" + }; } return \DBManager::get()->fetchAll( diff --git a/resources/vue/apps/forum/discussions/Edit.vue b/resources/vue/apps/forum/discussions/Edit.vue index ed8ab6b..26960b1 100644 --- a/resources/vue/apps/forum/discussions/Edit.vue +++ b/resources/vue/apps/forum/discussions/Edit.vue @@ -121,7 +121,7 @@ onMounted(() => {