From 3054c8cff250edc5ffcc4b1002ac5bd5b020afaf Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:07:40 +0000 Subject: [PATCH 1/4] feat(consumer-view): exclude AIB / bots-only tournament posts from consumer feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AIB (bots-only) questions are typically cross-posted in a human-facing tournament (e.g. Metaculus Cup), which produces duplicate-looking search results in the consumer view (issue #2605). Filter them out of `filter_for_consumer_view` using `Project.bot_leaderboard_status == BOTS_ONLY` — the same semantic marker the comment feed already uses to hide AIB-only comments (comments/services/feed.py:104-107). Using the flag instead of a hard-coded project id automatically covers each new quarterly AIB tournament. Co-authored-by: aseckin <3686968+aseckin@users.noreply.github.com> --- posts/services/feed.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/posts/services/feed.py b/posts/services/feed.py index 120cf61146..09108c73db 100644 --- a/posts/services/feed.py +++ b/posts/services/feed.py @@ -373,6 +373,13 @@ def filter_for_consumer_view(qs: QuerySet[Post]) -> QuerySet[Post]: # Exclude resolved questions qs = qs.exclude(resolved=True) + # Exclude AIB / bots-only tournaments — the same questions are typically + # cross-posted in a human-facing project (Metaculus Cup etc.), so showing + # them here produces duplicate-looking search results. + qs = qs.exclude( + default_project__bot_leaderboard_status=Project.BotLeaderboardStatus.BOTS_ONLY + ) + return qs From 0b8557b1345bbb1fbed96603e6a205e571eef561 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:36:19 +0000 Subject: [PATCH 2/4] fix(consumer-view): keep consumer-view filter applied during search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Text search was in EXPLICIT_FEED_FILTER_KEYS, so any search set hasExplicitFeedFilter=true and dropped for_consumer_view from the API call. That bypassed filter_for_consumer_view on the backend, letting AIB, resolved, and CP-hidden posts appear in search results for consumer-view users. Consumer view is a role-based constraint (user.interface_type === ConsumerView), not a browsing mode — searching shouldn't unset it. Co-authored-by: aseckin <3686968+aseckin@users.noreply.github.com> --- front_end/src/app/(main)/questions/helpers/filters.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/front_end/src/app/(main)/questions/helpers/filters.ts b/front_end/src/app/(main)/questions/helpers/filters.ts index 86f0082844..f6e816e52f 100644 --- a/front_end/src/app/(main)/questions/helpers/filters.ts +++ b/front_end/src/app/(main)/questions/helpers/filters.ts @@ -86,7 +86,6 @@ const EXPLICIT_FEED_FILTER_KEYS = [ POST_NOT_FORECASTER_ID_FILTER, POST_PROJECT_FILTER, POST_STATUS_FILTER, - POST_TEXT_SEARCH_FILTER, POST_TOPIC_FILTER, POST_TYPE_FILTER, POST_UPVOTED_BY_FILTER, From 4dda2f06305becba341415aca23ed492e0142a02 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:35:57 +0000 Subject: [PATCH 3/4] fix(consumer-view): keep resolved posts in search results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user is searching in consumer view they should still be able to find questions by title regardless of resolution state — otherwise a question they remember reading about disappears the moment it resolves. Co-authored-by: aseckin <3686968+aseckin@users.noreply.github.com> --- posts/services/feed.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/posts/services/feed.py b/posts/services/feed.py index 09108c73db..113f924bdd 100644 --- a/posts/services/feed.py +++ b/posts/services/feed.py @@ -70,7 +70,9 @@ def get_posts_feed( # noqa: C901 # Apply consumer views filter if for_consumer_view: - qs = filter_for_consumer_view(qs) + # When the user is searching, keep resolved posts in the result set so + # they can still find questions by title regardless of resolution state. + qs = filter_for_consumer_view(qs, include_resolved=bool(search)) # Exclude Deleted posts qs = qs.exclude(curation_status=Post.CurationStatus.DELETED) @@ -333,7 +335,9 @@ def get_posts_feed( # noqa: C901 return qs.distinct("id", order_type).only("pk") -def filter_for_consumer_view(qs: QuerySet[Post]) -> QuerySet[Post]: +def filter_for_consumer_view( + qs: QuerySet[Post], include_resolved: bool = False +) -> QuerySet[Post]: """ A special filter applied to default Consumer View feed representation https://github.com/Metaculus/metaculus/issues/3377 @@ -370,8 +374,10 @@ def filter_for_consumer_view(qs: QuerySet[Post]) -> QuerySet[Post]: ) ) - # Exclude resolved questions - qs = qs.exclude(resolved=True) + # Exclude resolved questions (unless the caller explicitly wants them, e.g. + # for search results where the user is looking up a specific question). + if not include_resolved: + qs = qs.exclude(resolved=True) # Exclude AIB / bots-only tournaments — the same questions are typically # cross-posted in a human-facing project (Metaculus Cup etc.), so showing From f72e42070ceb24ed91580572f5d0ee32f8c6065f Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 16 Jul 2026 08:22:03 +0200 Subject: [PATCH 4/4] Relax all consumer-view curation filters for search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consumer view hid notebooks outside programs/research, CP-pending questions and resolved questions from search, not just from the feed. Those filters curate what we offer someone browsing; when the user is searching they are looking up something specific, so they only produce dead ends. In practice the notebook filter was mostly hiding Metaculus' own editorial content (Pathways, Journal, meta, platform) from search. Replace include_resolved with an is_search flag that relaxes all three. The bots-only exclusion is a dedupe rather than curation — those posts are cross-posted into a human-facing project — so it now always applies, keeping the #2605 fix intact under search. Co-Authored-By: Claude Opus 4.8 --- posts/services/feed.py | 68 +++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/posts/services/feed.py b/posts/services/feed.py index 113f924bdd..c684443e3b 100644 --- a/posts/services/feed.py +++ b/posts/services/feed.py @@ -70,9 +70,7 @@ def get_posts_feed( # noqa: C901 # Apply consumer views filter if for_consumer_view: - # When the user is searching, keep resolved posts in the result set so - # they can still find questions by title regardless of resolution state. - qs = filter_for_consumer_view(qs, include_resolved=bool(search)) + qs = filter_for_consumer_view(qs, is_search=bool(search)) # Exclude Deleted posts qs = qs.exclude(curation_status=Post.CurationStatus.DELETED) @@ -336,52 +334,54 @@ def get_posts_feed( # noqa: C901 def filter_for_consumer_view( - qs: QuerySet[Post], include_resolved: bool = False + qs: QuerySet[Post], is_search: bool = False ) -> QuerySet[Post]: """ A special filter applied to default Consumer View feed representation https://github.com/Metaculus/metaculus/issues/3377 + + Curation filters shape what we offer someone browsing the feed, so they are + relaxed for search, where the user is looking up something specific. The + bots-only exclusion is a dedupe rather than curation and always applies: + those posts are cross-posted into a human-facing project, so keeping them + would surface duplicate-looking results. """ - now = timezone.now() + if not is_search: + now = timezone.now() - allowed_projects = list( - Project.objects.filter( - type=Project.ProjectTypes.NEWS_CATEGORY, - slug__in=["programs", "research"], + allowed_projects = list( + Project.objects.filter( + type=Project.ProjectTypes.NEWS_CATEGORY, + slug__in=["programs", "research"], + ) ) - ) - # Display only programs/research notebooks - qs = qs.filter( - Q(default_project__in=allowed_projects) - | Q(notebook__isnull=True) - | Q(projects__in=allowed_projects) - ) + # Display only programs/research notebooks + qs = qs.filter( + Q(default_project__in=allowed_projects) + | Q(notebook__isnull=True) + | Q(projects__in=allowed_projects) + ) - # We should keep posts where at least one question has CP revealed. - # For group questions, also check they're still open. - qs = qs.filter( - Q(notebook__isnull=False) - | Q(question__cp_reveal_time__lt=now) - | Exists( - Question.objects.filter( - Q(actual_close_time__isnull=True) | Q(actual_close_time__gte=now), - cp_reveal_time__lt=now, - group_id__isnull=False, - post_id=OuterRef("pk"), + # We should keep posts where at least one question has CP revealed. + # For group questions, also check they're still open. + qs = qs.filter( + Q(notebook__isnull=False) + | Q(question__cp_reveal_time__lt=now) + | Exists( + Question.objects.filter( + Q(actual_close_time__isnull=True) | Q(actual_close_time__gte=now), + cp_reveal_time__lt=now, + group_id__isnull=False, + post_id=OuterRef("pk"), + ) ) ) - ) - # Exclude resolved questions (unless the caller explicitly wants them, e.g. - # for search results where the user is looking up a specific question). - if not include_resolved: + # Exclude resolved questions qs = qs.exclude(resolved=True) - # Exclude AIB / bots-only tournaments — the same questions are typically - # cross-posted in a human-facing project (Metaculus Cup etc.), so showing - # them here produces duplicate-looking search results. qs = qs.exclude( default_project__bot_leaderboard_status=Project.BotLeaderboardStatus.BOTS_ONLY )