Skip to content

Question Page Redesign – Page shell#4646

Open
ncarazon wants to merge 10 commits intomainfrom
feat/question-page-redesign-1st-iteration
Open

Question Page Redesign – Page shell#4646
ncarazon wants to merge 10 commits intomainfrom
feat/question-page-redesign-1st-iteration

Conversation

@ncarazon
Copy link
Copy Markdown
Contributor

@ncarazon ncarazon commented Apr 23, 2026

Closes #4638

This PR delivers the first iteration of the Question Page redesign by introducing a new unified page shell architecture and rebuilding the main page structure for both Consumer and Forecaster views across desktop and mobile.

Implemented in Iteration 1

  • Built a new QuestionPageShell architecture that replaces legacy question layout components and centralizes page rendering logic
  • Introduced the new top-level layout structure:
    • Meta Row above the title with post metadata, chips, counters, and overflow handling
    • Title Row with updated title typography and responsive layout behavior
    • Action Row with pill-style actions and overflow menus for both variants
  • Preserved existing conditional post rendering behavior and wrapper support

UI / Layout updates

  • Reworked question page spacing, typography, alignment, and responsive behavior to match the new design system
  • Standardized post counters, chip styling, icons, and action button layouts
  • Added responsive overflow handling for chips and action menus
  • Implemented side-by-side layouts for prediction widgets and timelines where applicable
  • Updated mobile layouts for meta/action rows, prediction blocks, tab bars, comments, and section ordering

Sidebar & content changes

  • Reorganized desktop sidebar layouts for Consumer and Forecaster variants
  • Removed deprecated sidebar actions now covered by the new action row
  • Redesigned Similar Questions cards and layouts with question-type-specific charts and variant-specific actions
  • Added mobile-only Similar Questions and Timeline tabs where applicable

Functional improvements & fixes

  • Improved comments navigation, cross-tab scrolling, and key factor comment targeting behavior
  • Added conditional My Scores tab support for consumers
  • Fixed duplicate comments when switching tabs
  • Fixed chart/timeline layout issues, mobile clipping, spacing inconsistencies, and prediction block gaps
  • Refactored shared helpers and removed obsolete layout/view components

Cleanup

  • Removed legacy question page layout/view files and migrated Storybook stories to the new shell components

Summary by CodeRabbit

  • New Features

    • Added tabbed interface to question pages with dedicated sections for comments, timeline, key factors, and more
    • Introduced project tags display with responsive chip overflow handling
    • Enhanced similar questions section with improved variant support
    • Added direct comment scrolling functionality
  • UI/UX Improvements

    • Refreshed visual styling with improved typography and color scheme
    • Enhanced responsive layouts for mobile and desktop views
    • Redesigned question page shell architecture for better organization
  • Internationalization

    • Added translations for new UI elements across Czech, Spanish, Portuguese, Traditional Chinese, and Simplified Chinese

* feat: implement responsive meta row for question metrics and relocated project chips

* fix: sync meta row visibility breakpoints to prevent duplicated rendering and refine question header alignment layout

* refactor: replace trophy icon dedup, manual mousedown listener with HeadlessUI Popover, and use translations generate script

* fix: update Portuguese overflow label translation and fix vote score rendering condition
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Adds a new QuestionPageShell with tabs, refactors sidebar/similar-questions, updates comments and charts, removes legacy layouts, adds i18n strings, switches Storybook to shells, and updates backend serialization to include user forecasts.

Changes

Question Page Shell and integrations

Layer / File(s) Summary
i18n keys
front_end/messages/*
Adds projectsTags, questionLinks, and nMore strings.
Context/Contracts
...question_layout_context.tsx, ...comments_feed_provider.tsx, ...post_score_data/utils.ts
Shared activeTab and scroll-to-comment; reset pagination; helpers for post/user score visibility.
Key Factors integration
.../key_factors/*, ...single_question_score_data.tsx
Key-factor actions switch tabs and request comment scroll/reply; consumer section layout and scores wiring tweaks.
Shell and Tabs
...page_component.tsx, ...question_page_shell/*
New shell (TitleRow, MetaRow, ActionRow) with tab bar/panels; page uses shell.
Legacy removal
...question_layout/*, ...question_view/*
Removes old layout/view components and wrappers.
Sidebar/Similar
.../sidebar/*, .../similar_questions/*, components/icons/*, post_dropdown_menu.tsx
Variant-aware sidebar; similar-questions fetch+fallback; new cards/list; icons and dropdown tweak.
Comments/Charts
components/comment_feed/*, components/charts/*
Programmatic comment scroll; header/sort UI; x-axis textAnchor; mouseleave handler.
Actions/Timeline
.../question_view/action_row.tsx, ...timeline/*, predict button
Adds ActionRow; predict button accepts className; timeline props extended and spacing tweaks.
Scoring/Tiles/Meta UI
...group_forecast_card/*, ...detailed_question_card/*, post_status, post_voter, comment_status, private_note, voter, forecaster_counter, section_toggle
Compact group/date path; pass hideTitle; typography updates; showZeroVotes; hideToggle; counter bold.
Backend/API
services/api/posts.shared.ts, posts/serializers.py, posts/views.py
Similar-posts API without revalidate; include_user_forecasts support; richer similar-posts serialization.
Storybook
front_end/src/stories/question_page/*
Stories render via ConsumerShell/ForecasterShell with providers.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • elisescu
  • cemreinanc
  • lsabor
  • hlbmtc

Poem

I hop through tabs where comments flow,
A shiny shell with chips aglow;
The sidebar trims, the voters show,
Similar sparks in fallback rows.
Forecasts fetch and quietly grow—
Ship it swift, ears up, let’s go! 🐇✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/question-page-redesign-1st-iteration
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/question-page-redesign-1st-iteration

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 23, 2026

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4646-feat-question-page-redesign-1s-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:feat-question-page-redesign-1st-iteration-c936d3a
🗄️ PostgreSQL NeonDB branch preview/pr-4646-feat-question-page-redesign-1s
Redis Fly Redis mtc-redis-pr-4646-feat-question-page-redesign-1s

Details

  • Commit: c936d3a4e264401dc91c124efabe8312ba656d1d
  • Branch: feat/question-page-redesign-1st-iteration
  • Fly App: metaculus-pr-4646-feat-question-page-redesign-1s

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

* feat(ui): build new TitleRow component with updated typography and variant-specific layout logic

* fix: move question info on top in forecaster header
* feat: added new question action row with unified pill styling, dynamic primary logic, and add custom icons

* refactor: consolidate pill styles into CVA + PillButton, replace hardcoded hex with design tokens, and fix Follow active state styling

* refactor: remove action buttons from sidebar

* fix: add dark mode variants to all pill styles

* fix: remove dead imports and stale questionTitle prop from Sidebar
…/action rows (#4657)

* feat: added new question action row with unified pill styling, dynamic primary logic, and add custom icons

* refactor: consolidate pill styles into CVA + PillButton, replace hardcoded hex with design tokens, and fix Follow active state styling

* fix: remove dead imports and stale questionTitle prop from Sidebar

* feat: filter PostDropdownMenu for desktop and polish mobile consumer/forecaster meta and action rows

* fix: show full action row from 768px, adjust Predict/Share on mobile, and gate dropdown Share/Follow/Embed below md
feat: add desktop tab bar with shared active-tab state
feat: add tab content adapters
* feat: change sidebar reflow for forecaster and consumer views

* feat: redesign similar questions sidebar with new card layout and charts

* feat: differentiate similar questions chart display between forecaster and consumer views

* feat: add vertical bar chart for date/numeric group questions in similar questions consumer sidebar

* fix: disable frontend cache for similar posts endpoint to ensure per-user vote state is returned correctly

* fix: limit vertical bar chart to visible choices count to prevent overlapping labels

* fix: replace VerticalBarConsumerCard with GroupForecastCard in similar questions sidebar and remove hideResolutionIcon prop

* Similar questions endpoint: added user context to the API response

---------

Co-authored-by: hlbmtc <hlib@metaculus.com>
* feat: integrate shell, widen column to 59rem, delete dead layouts

* fix: CommunityDisclaimer placement and add shell z-index

* feat: replace direct DOM scrolling with context-based comment navigation, improve cross-tab comment access, and refine consumer shell layout handling for different question types

* feat: refine question page and comment feed styling, spacing, and layout

* fix: stack comment date below author name on question page and align date/vote typography to design spec

* feat: add My Scores tab for consumer, wire PostScoreData into forecaster shell with Resolution Criteria and Background Info, and fix duplicate comments on tab re-entry

* refactor: gate My Scores tab on user scores, extract comment header condition into named vars, and deduplicate post score routing logic

* fix: forward mobileSidebar to ConsumerShell, fix translated banner timer cleanup, update my-scores hash key, and widen Storybook story wrappers to 59rem

* fix: restore preselectedGroupQuestionId in ConsumerShell for group and fan graph questions
* fix: mobile consumer layout: center title, show prediction for all question types, fix comments tab blank below lg

* feat: consumer view mobile - typography, spacing, tab bar sizing, horizontal scroll, and timeline in tab on mobile

* feat: adjust title typography, vote count on mobile, and restore Forecast Timeline label across all views

* feat: consumer/forecaster mobile - similar questions tab, hide sidebar author section on mobile, shared chip helpers with TrophyIcon, fix continuous graph clipping, and consumer title right padding

* feat: consumer mobile - remove group prediction top margin on mobile and show compact bar chart for date groups instead of scatter

* fix: format posts/views.py

* fix: mobile polish pass, vote count size, comment text styling, section toggle arrow, and key factors link

* fix: restrict fan graph/date group bottom margin to desktop only

* fix: align x-axis tick labels with chart boundaries

* fix: restructure consumer chart layout for date/fan graph groups and fix CommunityDisclaimer to only show for Community tournament type

* refactor: migrate similar posts fetching to TanStack Query, co-located in SimilarQuestionsTab

* feat: fall back to top-50 hot questions when no similar questions exist

* fix: skip top-posts fallback fetch when similar questions already exist

* fix: improve top-questions fallback with correct fetch params, loading state, and error handling

* fix: sort tailwind classes to satisfy prettier lint rule
@ncarazon ncarazon changed the title Redesigned Question page - Page shell Question Page Redesign – Page shell May 7, 2026
@ncarazon ncarazon marked this pull request as ready for review May 7, 2026 09:12
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
posts/views.py (1)

104-116: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

include_user_forecasts=True on the feed endpoint: verify necessity and performance cost

with_cp in posts_list_api_view has no default — when a client omits it the flag silently has no effect (see the serializer comment). When with_cp=true IS requested, every authenticated feed page now also executes prefetch_user_forecasts(current_user.id) across all paginated posts. If feed cards don't actually render my_forecasts data, this is a free performance regression. Please confirm the feed-card UI consumes this data before keeping the flag here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@posts/views.py` around lines 104 - 116, The feed view is unconditionally
passing include_user_forecasts=True to serialize_post_many which triggers
prefetch_user_forecasts whenever with_cp is requested, causing a potential perf
regression; verify whether the feed card UI actually renders my_forecasts and if
not remove the flag, otherwise gate it so you only pass
include_user_forecasts=True when with_cp is truthy and the user is authenticated
and the UI requires forecasts (e.g., check request.user.is_authenticated and the
with_cp flag or a dedicated query param before calling serialize_post_many);
also consider adding a default for with_cp handling in posts_list_api_view or
explicitly reading request.GET to avoid the silent no-op described.
front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/similar_question_prediction_chip.tsx (1)

91-98: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing return null fallback in the forecaster branch

The consumer branch explicitly returns null for unmatched posts (line 57), but the forecaster branch falls off the end if none of isMultipleChoicePost, isQuestionPost, or isGroupOfQuestionsPost match. React will throw a runtime error if a component returns undefined.

🐛 Proposed fix
  if (isGroupOfQuestionsPost(post)) {
    return (
      <div className="w-full">
        <GroupOfQuestionsTile post={post} showChart={false} />
      </div>
    );
  }
+
+  return null;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/questions/[id]/components/sidebar/similar_questions/similar_question_prediction_chip.tsx
around lines 91 - 98, The forecaster branch in SimilarQuestionPredictionChip can
fall through and return undefined when none of isMultipleChoicePost,
isQuestionPost, or isGroupOfQuestionsPost match; update the component to
explicitly return null as a fallback (after the GroupOfQuestionsTile branch) so
it mirrors the consumer branch's behavior and avoids React runtime errors.
🧹 Nitpick comments (8)
front_end/src/app/(main)/questions/components/forecaster_counter.tsx (1)

65-70: 💤 Low value

strong passthrough silently strips RichText's default strong renderer when boldCount is false.

The explicit strong key at line 65 overrides whatever handler ...tags (spread from RichText) injected for <strong>. When boldCount is false, raw chunks are returned unwrapped, discarding the default rendering contract from RichText. If RichText's strong tag ever applies its own styling (e.g. font-semibold), the non-bold path will silently drop it.

If the intent is strictly "bold only when boldCount=true, otherwise no emphasis at all", this is correct. Otherwise, consider falling back to tags.strong:

♻️ Proposed defensive passthrough
-                strong: (chunks) =>
-                  boldCount ? (
-                    <span className="font-bold">{chunks}</span>
-                  ) : (
-                    chunks
-                  ),
+                strong: boldCount
+                  ? (chunks) => <span className="font-bold">{chunks}</span>
+                  : tags.strong,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/questions/components/forecaster_counter.tsx around
lines 65 - 70, The current custom `strong` renderer in the forecaster_counter
component overrides RichText's default handler and returns raw `chunks` when
`boldCount` is false, dropping any default styling; update the `strong`
passthrough so that when `boldCount` is true it wraps with your bold markup,
otherwise it calls and returns the original `tags.strong` handler (or the
default renderer) so RichText's intended styling/behavior is preserved; look for
the `strong: (chunks) =>` entry near the `...tags` spread and change the false
branch to delegate to `tags.strong` (or equivalent) instead of returning raw
`chunks`.
posts/views.py (1)

567-583: with_cp=True on the similar-posts endpoint conflicts with its own "don't overload Redis" comment

The endpoint previously had a lighter serialization call specifically to avoid Redis pressure. The new with_cp=True triggers prefetch_questions_scores() for every similar-post lookup. group_cutoff=1 helps, but the change is still a net increase. Consider whether CP data is strictly required here for the sidebar design, or whether a lighter approach (e.g., skipping with_cp, or caching the response at the view level) would be sufficient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@posts/views.py` around lines 567 - 583, The similar-posts view
post_similar_posts_api_view currently calls serialize_post_many(...,
with_cp=True) which forces prefetch_questions_scores and increases Redis load;
change this to a lighter serialization by removing or setting with_cp=False (or
add a flag to only include CP when explicitly requested) so serialize_post_many
is called without triggering prefetch_questions_scores, or alternatively
implement view-level caching around the serialized result in
post_similar_posts_api_view to avoid repeated CP prefetches; update the call
site (serialize_post_many in post_similar_posts_api_view) and any tests that
assume CP fields are present.
front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/comments.tsx (1)

12-16: ⚖️ Poor tradeoff

Consider adding a flat/no-container-styles prop to CommentFeed instead of the !important selector chain.

The wrapper div's [&>section]:!* overrides are fragile — they silently break if CommentFeed's internal element ever changes from <section> to something else. A noContainerStyles (or similar) prop on CommentFeed would be more resilient and expressive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/tabs/comments.tsx
around lines 12 - 16, The wrapper uses fragile tailwind child selector overrides
on <section> to remove container styles; instead add a boolean prop (e.g.,
noContainerStyles or flat) to the CommentFeed component and have CommentFeed
apply the containerless classes internally when that prop is true. Update the
CommentsTab component to pass noContainerStyles (or flat) to CommentFeed and
remove the `[&>section]:!*` overrides so the styling control is explicit and
resilient to internal markup changes.
front_end/src/app/(main)/questions/[id]/components/question_page_shell/meta_row.tsx (1)

49-54: 💤 Low value

Initial render shows default chip count (2) regardless of container width.

When width === 0 (before the resize observer fires on first paint), maxVisibleChips evaluates to 2 and compactCounters to false. This is fine as a safe default, but it will cause a visible layout shift on narrow containers. Consider initializing useContainerSize or the width thresholds more conservatively (e.g., default maxVisibleChips to 1) if the layout shift is noticeable in practice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/meta_row.tsx
around lines 49 - 54, The initial render uses width === 0 which causes
maxVisibleChips to default to 2 and compactCounters to false, creating a layout
shift; update the logic in the component around compactCounters and
maxVisibleChips (the variables computed from width, and where useContainerSize
provides width) to treat width <= 0 as an unknown/initial state and return
conservative defaults (e.g., set maxVisibleChips = 1 and compactCounters = true
when width <= 0) so the first paint matches narrow-container behavior until the
resize observer fires.
front_end/src/stories/question_page/date_question/date_question.stories.tsx (1)

76-76: 💤 Low value

buildKey can return an empty string.

When no transform rules apply, appliedKeys is empty and this returns "" as the key, which can be problematic for React reconciliation. Other migrated stories in this PR (e.g., binary_group_timeline.stories.tsx) use a "default" fallback. Consider aligning for consistency:

Proposed alignment
-  buildKey: (_, appliedKeys) => `${appliedKeys.join("-")}`,
+  buildKey: (_, appliedKeys) =>
+    appliedKeys.length > 0 ? appliedKeys.join("-") : "default",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/stories/question_page/date_question/date_question.stories.tsx`
at line 76, The buildKey function currently returns an empty string when no
transform rules apply because appliedKeys is empty; update the buildKey
implementation (the buildKey property in date_question.stories.tsx) to return a
non-empty fallback (e.g., "default") when appliedKeys.length === 0 so React
reconciliation is stable and consistent with other stories like
binary_group_timeline.stories.tsx.
front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/similar_question_card.tsx (1)

109-136: 💤 Low value

Forecaster footer chip styling looks slightly inconsistent.

Three sibling chips share the same row but use different backgrounds/padding:

  • Comment chip (Line 112): rounded-xs bg-gray-200 px-1.5
  • Status icon container (Line 119): rounded-xs bg-gray-200 px-1 (narrower padding)
  • Forecasters chip (Line 128): no background, no rounding (text-xs leading-4 text-gray-700 only)

If this is the intended design (e.g., status badge gets tighter padding, forecasters count is unboxed), feel free to ignore. Otherwise consider unifying them, since visually adjacent counters typically benefit from a single chip style.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/questions/[id]/components/sidebar/similar_questions/similar_question_card.tsx
around lines 109 - 136, The three adjacent footer chips in SimilarQuestionCard
(rendered when isForecaster) use inconsistent classes: the comment span, the
PostStatusIcon container, and the forecasters span; unify their appearance by
applying a consistent chip class set (e.g., "flex h-6 items-center gap-1
rounded-xs bg-gray-200 px-1.5 text-xs leading-4 text-gray-700
dark:bg-gray-200-dark dark:text-gray-700-dark") to the comment span, the wrapper
div around PostStatusIcon, and the forecasters span so PostVoter, PostStatusIcon
wrapper, and the forecasters count (forecastersFormatted) all share the same
background, padding, rounding and text styles.
front_end/src/app/(main)/questions/[id]/components/question_page_shell/title_row.tsx (1)

42-55: 💤 Low value

Remove ineffective order-* utilities from single-child flex container.

The div at line 43 is the only child within its flex parent (line 42), so order-1 lg:order-0 are dead code—CSS order only reorders flex items relative to siblings. Additionally, lg:order-0 is not part of Tailwind's default order scale (which supports order-1order-12, order-first, order-last, order-none) and the project config does not extend this scale, so lg:order-0 silently produces no CSS.

If reordering the title row relative to the desktop CP status block at line 56 was intended, these utilities should be applied to siblings at the parent level (line 38), not nested inside line 42.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/title_row.tsx
around lines 42 - 55, The order utilities on the inner container (the div with
class "lg:order-0 order-1 flex items-center") are ineffective because its parent
(the div with class "flex flex-1 flex-col") has only that single child; remove
the `order-1` and `lg:order-0` classes from that inner div. If you intended to
reorder the title vs. the desktop CP status (components QuestionTitle and
QuestionHeaderCPStatus), move the order classes to the relevant sibling
containers at the parent level so the `order-*` utilities affect sibling
ordering instead of applying them on this single-child element.
front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx (1)

77-84: ⚡ Quick win

Duplicate CommunityDisclaimer block — consider extracting to a helper

The exact same CommunityDisclaimer snippet appears in both ForecasterShell (lines 77-84) and ConsumerShell (lines 175-182). A small helper or shared fragment would remove the duplication.

♻️ Proposed refactor
+const CommunityBanner: FC<{ post: PostWithForecasts }> = ({ post }) =>
+  post.projects?.default_project?.type === TournamentType.Community ? (
+    <CommunityDisclaimer
+      project={post.projects.default_project}
+      variant="standalone"
+      className="block sm:hidden"
+    />
+  ) : null;

 // ForecasterShell
-          {postData.projects?.default_project?.type ===
-            TournamentType.Community && (
-            <CommunityDisclaimer
-              project={postData.projects.default_project}
-              variant="standalone"
-              className="block sm:hidden"
-            />
-          )}
+          <CommunityBanner post={postData} />

 // ConsumerShell
-        {postData.projects?.default_project?.type ===
-          TournamentType.Community && (
-          <CommunityDisclaimer
-            project={postData.projects.default_project}
-            variant="standalone"
-            className="block sm:hidden"
-          />
-        )}
+        <CommunityBanner post={postData} />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/index.tsx
around lines 77 - 84, Duplicate JSX rendering of CommunityDisclaimer exists in
ForecasterShell and ConsumerShell; extract it into a small helper/component
(e.g., renderCommunityDisclaimer or CommunityDisclaimerFragment) and replace
both inline blocks with a single call. The helper should accept the same props
used now (project: postData.projects.default_project, variant: "standalone",
className: "block sm:hidden") and perform the conditional check
postData.projects?.default_project?.type === TournamentType.Community before
returning <CommunityDisclaimer /> so both ForecasterShell and ConsumerShell
simply invoke the helper to remove duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front_end/messages/cs.json`:
- Line 1823: The Czech value for the translation key questionLinks reads
awkwardly; update the JSON entry for "questionLinks" to use a natural
prepositional form—replace "Odkazy otázky" with "Odkazy na otázky" so the UI
reads correctly. Ensure you update the string value for the questionLinks key in
front_end/messages/cs.json accordingly and keep JSON quoting/commas intact.

In `@front_end/src/app/`(main)/questions/[id]/components/post_score_data/utils.ts:
- Around line 23-28: shouldQuestionShowUserScores currently returns true
whenever question.my_forecasts?.score_data exists, but it lacks the
unsuccessful-resolution guard present in shouldQuestionShowScores; update
shouldQuestionShowUserScores to also return false when
isUnsuccessfullyResolved(question.resolution) is true (i.e., add a check like
!isUnsuccessfullyResolved(question.resolution) combined with the existing
user_scores check), referencing the same question.my_forecasts?.score_data and
isUnsuccessfullyResolved symbol to ensure the "My Scores" tab stays hidden for
annulled/ambiguous resolutions.

In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/tab_bar.tsx:
- Around line 95-99: The UI shows a different highlighted tab than the panel
because the local `active` fallback in tab_bar.tsx is not syncing back to the
layout context when the current `activeTab` is removed; add a useEffect in the
TabBar component that watches `active` and `activeTab`/`tabs` and, when
`activeTab` is not present in `tabs`, calls the context updater (e.g.,
`setActiveTab`) to set the context to the computed fallback `active` so
`renderActivePanel` in tabs.tsx reads the same value the <Tabs> control
displays.

In `@front_end/src/components/charts/primitives/x_tick_label.tsx`:
- Around line 26-29: The right-boundary check is asymmetric: when withCursor is
false the code uses a fixed 12px threshold causing wide labels
(estimatedTextWidth > 12) to overflow; in the conditional around x/chartWidth
replace the ternary so both branches use a half-width-aware guard (e.g. x >
chartWidth - estimatedTextWidth) or, if you deliberately want a positional 12px
guard when withCursor is false, add a clarifying comment explaining why the
thresholds differ; update the expression that sets textAnchor (the block
referencing withCursor, x, chartWidth, estimatedTextWidth, and textAnchor)
accordingly.

In `@front_end/src/components/comment_feed/comment.tsx`:
- Around line 1081-1083: Replace the hardcoded aria-label on the dropdown
trigger Button with a localized string: call useTranslations() at the top of the
component (e.g., const t = useTranslations()) and change aria-label="menu" to
aria-label={t("moreOptions") || t("menu")}, ensuring the key exists in your i18n
files; update the component that renders the Button (the Comment component / the
dropdown trigger Button) and add the necessary import for useTranslations to the
file.

In `@front_end/src/components/comment_feed/index.tsx`:
- Around line 348-351: The sort/count row is rendering regardless of compact
mode; wrap or gate the sort-and-count UI using the same compactVersion check as
showCommentHeader (i.e., ensure the sort/count block is only rendered when
!compactVersion). Locate the sort/count JSX near the comment header rendering
(references: showCommentHeader, compactVersion) and move it inside the same
conditional or add a condition like !compactVersion so callers such as
ResponsiveCommentFeed receive a true compact view with those elements
suppressed.

In `@front_end/src/components/post_card/basic_post_card/comment_status.tsx`:
- Line 57: The className string contains a merged token "border-nonepx-2" which
should be split and de-duplicated; update the string so it contains "border-none
px-2" (remove the duplicate extra "border-none" if present elsewhere in that
same string) so Tailwind correctly applies the px-2 horizontal padding; locate
and fix the literal class string in comment_status.tsx where "border-nonepx-2
h-6..." appears.

In `@front_end/src/components/post_card/basic_post_card/post_voter.tsx`:
- Around line 80-88: The issue is the hardcoded "md:text-sm" in the static class
string overriding the compact branch at desktop; update the class logic in
post_voter.tsx so responsive sizing can be controlled by the compact prop:
remove the fixed "md:text-sm" and use a non-responsive "text-sm" as the default,
then let the compact conditional (compact ? "text-xs" : "text-sm") determine
sizing (or, alternatively, move "md:text-sm" into the non-compact branch).
Adjust the cn(...) call around the vote score span accordingly so compact=true
wins at md+.

In `@posts/serializers.py`:
- Around line 506-507: The prefetch of user forecasts is currently nested under
the with_cp conditional so include_user_forecasts=True can silently do nothing;
either document this dependency in the function signature or enforce it — e.g.,
add an assertion that include_user_forecasts implies with_cp, or move the call
to qs.prefetch_user_forecasts(current_user.id) out of the with_cp block so
include_user_forecasts takes effect independently; update the relevant function
signature/docstring and callers (e.g., posts_list_api_view) accordingly.

---

Outside diff comments:
In
`@front_end/src/app/`(main)/questions/[id]/components/sidebar/similar_questions/similar_question_prediction_chip.tsx:
- Around line 91-98: The forecaster branch in SimilarQuestionPredictionChip can
fall through and return undefined when none of isMultipleChoicePost,
isQuestionPost, or isGroupOfQuestionsPost match; update the component to
explicitly return null as a fallback (after the GroupOfQuestionsTile branch) so
it mirrors the consumer branch's behavior and avoids React runtime errors.

In `@posts/views.py`:
- Around line 104-116: The feed view is unconditionally passing
include_user_forecasts=True to serialize_post_many which triggers
prefetch_user_forecasts whenever with_cp is requested, causing a potential perf
regression; verify whether the feed card UI actually renders my_forecasts and if
not remove the flag, otherwise gate it so you only pass
include_user_forecasts=True when with_cp is truthy and the user is authenticated
and the UI requires forecasts (e.g., check request.user.is_authenticated and the
with_cp flag or a dedicated query param before calling serialize_post_many);
also consider adding a default for with_cp handling in posts_list_api_view or
explicitly reading request.GET to avoid the silent no-op described.

---

Nitpick comments:
In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/index.tsx:
- Around line 77-84: Duplicate JSX rendering of CommunityDisclaimer exists in
ForecasterShell and ConsumerShell; extract it into a small helper/component
(e.g., renderCommunityDisclaimer or CommunityDisclaimerFragment) and replace
both inline blocks with a single call. The helper should accept the same props
used now (project: postData.projects.default_project, variant: "standalone",
className: "block sm:hidden") and perform the conditional check
postData.projects?.default_project?.type === TournamentType.Community before
returning <CommunityDisclaimer /> so both ForecasterShell and ConsumerShell
simply invoke the helper to remove duplication.

In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/meta_row.tsx:
- Around line 49-54: The initial render uses width === 0 which causes
maxVisibleChips to default to 2 and compactCounters to false, creating a layout
shift; update the logic in the component around compactCounters and
maxVisibleChips (the variables computed from width, and where useContainerSize
provides width) to treat width <= 0 as an unknown/initial state and return
conservative defaults (e.g., set maxVisibleChips = 1 and compactCounters = true
when width <= 0) so the first paint matches narrow-container behavior until the
resize observer fires.

In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/tabs/comments.tsx:
- Around line 12-16: The wrapper uses fragile tailwind child selector overrides
on <section> to remove container styles; instead add a boolean prop (e.g.,
noContainerStyles or flat) to the CommentFeed component and have CommentFeed
apply the containerless classes internally when that prop is true. Update the
CommentsTab component to pass noContainerStyles (or flat) to CommentFeed and
remove the `[&>section]:!*` overrides so the styling control is explicit and
resilient to internal markup changes.

In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/title_row.tsx:
- Around line 42-55: The order utilities on the inner container (the div with
class "lg:order-0 order-1 flex items-center") are ineffective because its parent
(the div with class "flex flex-1 flex-col") has only that single child; remove
the `order-1` and `lg:order-0` classes from that inner div. If you intended to
reorder the title vs. the desktop CP status (components QuestionTitle and
QuestionHeaderCPStatus), move the order classes to the relevant sibling
containers at the parent level so the `order-*` utilities affect sibling
ordering instead of applying them on this single-child element.

In
`@front_end/src/app/`(main)/questions/[id]/components/sidebar/similar_questions/similar_question_card.tsx:
- Around line 109-136: The three adjacent footer chips in SimilarQuestionCard
(rendered when isForecaster) use inconsistent classes: the comment span, the
PostStatusIcon container, and the forecasters span; unify their appearance by
applying a consistent chip class set (e.g., "flex h-6 items-center gap-1
rounded-xs bg-gray-200 px-1.5 text-xs leading-4 text-gray-700
dark:bg-gray-200-dark dark:text-gray-700-dark") to the comment span, the wrapper
div around PostStatusIcon, and the forecasters span so PostVoter, PostStatusIcon
wrapper, and the forecasters count (forecastersFormatted) all share the same
background, padding, rounding and text styles.

In `@front_end/src/app/`(main)/questions/components/forecaster_counter.tsx:
- Around line 65-70: The current custom `strong` renderer in the
forecaster_counter component overrides RichText's default handler and returns
raw `chunks` when `boldCount` is false, dropping any default styling; update the
`strong` passthrough so that when `boldCount` is true it wraps with your bold
markup, otherwise it calls and returns the original `tags.strong` handler (or
the default renderer) so RichText's intended styling/behavior is preserved; look
for the `strong: (chunks) =>` entry near the `...tags` spread and change the
false branch to delegate to `tags.strong` (or equivalent) instead of returning
raw `chunks`.

In `@front_end/src/stories/question_page/date_question/date_question.stories.tsx`:
- Line 76: The buildKey function currently returns an empty string when no
transform rules apply because appliedKeys is empty; update the buildKey
implementation (the buildKey property in date_question.stories.tsx) to return a
non-empty fallback (e.g., "default") when appliedKeys.length === 0 so React
reconciliation is stable and consistent with other stories like
binary_group_timeline.stories.tsx.

In `@posts/views.py`:
- Around line 567-583: The similar-posts view post_similar_posts_api_view
currently calls serialize_post_many(..., with_cp=True) which forces
prefetch_questions_scores and increases Redis load; change this to a lighter
serialization by removing or setting with_cp=False (or add a flag to only
include CP when explicitly requested) so serialize_post_many is called without
triggering prefetch_questions_scores, or alternatively implement view-level
caching around the serialized result in post_similar_posts_api_view to avoid
repeated CP prefetches; update the call site (serialize_post_many in
post_similar_posts_api_view) and any tests that assume CP fields are present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ce74cd31-71e0-45ff-b0de-a081f967046e

📥 Commits

Reviewing files that changed from the base of the PR and between 691858b and c936d3a.

📒 Files selected for processing (89)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/src/app/(main)/components/comments_feed_provider.tsx
  • front_end/src/app/(main)/questions/[id]/[[...slug]]/page_component.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/key_factor_header.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/more_panel.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/key_factor_detail_overlay.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/key_factors_question_consumer_section.tsx
  • front_end/src/app/(main)/questions/[id]/components/post_score_data/single_question_score_data.tsx
  • front_end/src/app/(main)/questions/[id]/components/post_score_data/utils.ts
  • front_end/src/app/(main)/questions/[id]/components/question_layout/consumer_question_layout/consumer_tabs.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/consumer_question_layout/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/consumer_question_layout/responsive_comment_feed.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/forecaster_question_layout/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/question_info.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/question_layout_context.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/question_section.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/meta_row.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/project_chip_helpers.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tab_bar.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/comments.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/key_factors.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/my_scores.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/private_notes.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/question_info.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/question_links.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/similar_questions.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/timeline.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/title_row.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/action_row.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/action_buttons/question_predict_button.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/prediction/group_of_questions_prediction/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/prediction/single_question_prediction/binary_question_prediction.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/prediction/single_question_prediction/continuous_question_prediction.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/timeline/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/sidebar_container.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/sidebar_question_info.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/similar_question_card.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/similar_question_prediction_chip.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/similar_questions_drawer.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/similar_questions_list.tsx
  • front_end/src/app/(main)/questions/components/coherence_links/coherence_links.tsx
  • front_end/src/app/(main)/questions/components/forecaster_counter.tsx
  • front_end/src/components/charts/group_chart.tsx
  • front_end/src/components/charts/primitives/x_tick_label.tsx
  • front_end/src/components/comment_feed/comment.tsx
  • front_end/src/components/comment_feed/comment_card.tsx
  • front_end/src/components/comment_feed/comment_date.tsx
  • front_end/src/components/comment_feed/comment_voter.tsx
  • front_end/src/components/comment_feed/comment_wrapper.tsx
  • front_end/src/components/comment_feed/index.tsx
  • front_end/src/components/consumer_post_card/group_forecast_card/index.tsx
  • front_end/src/components/detailed_question_card/detailed_question_card/index.tsx
  • front_end/src/components/icons/share.tsx
  • front_end/src/components/icons/trophy.tsx
  • front_end/src/components/post_actions/post_dropdown_menu.tsx
  • front_end/src/components/post_card/basic_post_card/comment_status.tsx
  • front_end/src/components/post_card/basic_post_card/post_voter.tsx
  • front_end/src/components/post_card/question_tile/question_continuous_tile.tsx
  • front_end/src/components/post_status/index.tsx
  • front_end/src/components/question/private_note.tsx
  • front_end/src/components/ui/section_toggle.tsx
  • front_end/src/components/voter.tsx
  • front_end/src/services/api/posts/posts.shared.ts
  • front_end/src/stories/question_page/binary_group/binary_group_timeline.stories.tsx
  • front_end/src/stories/question_page/binary_group/fan_chart.stories.tsx
  • front_end/src/stories/question_page/binary_question/binary_question.stories.tsx
  • front_end/src/stories/question_page/continuous_question/continuous_question.stories.tsx
  • front_end/src/stories/question_page/date_group/date_group_timeline.stories.tsx
  • front_end/src/stories/question_page/date_group/fan_chart.stories.tsx
  • front_end/src/stories/question_page/date_question/date_question.stories.tsx
  • front_end/src/stories/question_page/mc_question/mc_question.stories.tsx
  • front_end/src/stories/question_page/numeric_group/numeric_fan_chart.stories.tsx
  • front_end/src/stories/question_page/numeric_group/numeric_group_timeline.stories.tsx
  • posts/serializers.py
  • posts/views.py
💤 Files with no reviewable changes (11)
  • front_end/src/app/(main)/questions/[id]/components/question_layout/consumer_question_layout/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/similar_questions_drawer.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/forecaster_question_layout/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/question_section.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/consumer_question_layout/consumer_tabs.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/question_info.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_layout/index.tsx

"loadMore": "Načíst více",
"noPrivateNotes": "Žádné soukromé poznámky",
"privateNotes": "Soukromé poznámky",
"questionLinks": "Odkazy otázky",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix Czech grammar for questionLinks translation.

"Odkazy otázky" reads awkwardly in Czech. Use a preposition form to match natural UI phrasing.

✏️ Suggested fix
-  "questionLinks": "Odkazy otázky",
+  "questionLinks": "Odkazy na otázku",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/messages/cs.json` at line 1823, The Czech value for the translation
key questionLinks reads awkwardly; update the JSON entry for "questionLinks" to
use a natural prepositional form—replace "Odkazy otázky" with "Odkazy na otázky"
so the UI reads correctly. Ensure you update the string value for the
questionLinks key in front_end/messages/cs.json accordingly and keep JSON
quoting/commas intact.

Comment on lines +23 to +28
export const shouldQuestionShowUserScores = (
question: QuestionWithForecasts
) => {
const userScores = question.my_forecasts?.score_data;
return !isNil(userScores) && Object.keys(userScores).length > 0;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what resolution values backend can produce alongside non-empty score_data
rg -n "score_data" --type=py -A3 -B3

Repository: Metaculus/metaculus

Length of output: 7370


🏁 Script executed:

#!/bin/bash
# Get more context around the score_data population in common.py
sed -n '700,760p' questions/serializers/common.py | cat -n

Repository: Metaculus/metaculus

Length of output: 2929


🏁 Script executed:

#!/bin/bash
# Search for where scores are queried/fetched in the serializer
rg -n "def.*serializer\|scores\s*=" questions/serializers/common.py -A2 -B2 | head -100

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for resolution-based filtering in score queries
rg -n "resolution\|isUnsuccessfullyResolved\|annulled\|ambiguous" questions/serializers/common.py

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Find the function signature and see where scores come from
rg -n "def.*serialize.*question\|scores = " questions/serializers/common.py -B5 -A30 | head -150

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for the query that retrieves scores (likely a .filter() call)
rg -n "\.filter\(.*resolution\)|\.exclude\(.*resolution\)|scores.*=" questions/ --type=py -B2 -A2 | grep -A5 -B5 "score"

Repository: Metaculus/metaculus

Length of output: 1463


🏁 Script executed:

#!/bin/bash
# Find the definition of user_scores and user_archived_scores properties
rg -n "def user_scores|user_scores\s*=|@property" questions/models.py -A5 | grep -A10 "user_scores\|user_archived_scores"

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Alternative: search in all Python files under questions
fd -e py questions/ -exec grep -l "user_scores\|user_archived_scores" {} \;

Repository: Metaculus/metaculus

Length of output: 332


🏁 Script executed:

#!/bin/bash
# Search for user_scores definition across codebase
rg -n "user_scores\s*=" questions/ --type=py -B2 -A8

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Also search for it as a property decorator
rg -n "@property" questions/models.py -A10 | grep -A12 "user_scores"

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check Question model structure
rg -n "class Question" questions/models.py -A50 | head -100

Repository: Metaculus/metaculus

Length of output: 3483


🏁 Script executed:

#!/bin/bash
# Search for where user_scores is annotated
rg -n "user_scores\|user_archived_scores" questions/ --type=py -B3 -A3 | grep -B5 -A5 "annotate\|Subquery"

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for views or serializers that set up these annotations
rg -n "user_scores.*=" questions/views/ --type=py -B2 -A10

Repository: Metaculus/metaculus

Length of output: 120


🏁 Script executed:

#!/bin/bash
# Search in views.py for annotate calls related to scores
rg -n "\.annotate\(" questions/views.py -A20 | grep -A20 "score"

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# List structure of questions directory
find questions -name "*.py" -type f | head -30

Repository: Metaculus/metaculus

Length of output: 1589


🏁 Script executed:

#!/bin/bash
# Search all Python files for where user_scores annotation is added
rg -n "\.annotate\(.*user_scores\|user_scores.*=" . --type=py -B3 -A3

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Try searching in serializers for where these are pulled
rg -n "user_scores" . --type=py -B5 -A5 | head -100

Repository: Metaculus/metaculus

Length of output: 6271


🏁 Script executed:

#!/bin/bash
# Search for where question objects are prepared before being serialized
rg -n "\.annotate\(" questions/ --type=py -B2 -A10 | head -200

Repository: Metaculus/metaculus

Length of output: 1622


🏁 Script executed:

#!/bin/bash
# Look for where SerializerQuestion is called or used
rg -n "SerializeQuestion\|serialize_question\|QuestionWithForecasts" . --type=py -B3 -A3 | head -150

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if there's a view that prepares the question queryset
rg -n "class.*View\|def.*question" questions/views.py --type=py -B2 -A15 2>/dev/null | head -100

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for Score model and where it gets created/filtered
rg -n "class Score" questions/models.py -A20

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Search for view endpoints that return question data with scores
rg -n "def.*question" . --type=py -B2 -A10 | grep -B10 -A10 "serialize_question\|my_forecasts"

Repository: Metaculus/metaculus

Length of output: 5849


🏁 Script executed:

#!/bin/bash
# Check the API endpoint that would serialize this question for frontend
rg -n "QuestionWithForecasts\|my_forecasts" front_end --type=ts --type=tsx -B3 -A3 | head -50

Repository: Metaculus/metaculus

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for where annotate happens with scores - look in views
find . -name "views.py" -type f | xargs grep -l "user_scores\|annotate" 2>/dev/null

Repository: Metaculus/metaculus

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Look in the API/views to see how questions are queried before serialization
rg -n "def get_question\|\.prefetch\|\.select_related" . --type=py -B3 -A15 | grep -B10 -A10 "user_scores\|scores"

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check the Score model definition to understand what it represents
rg -n "^class Score\|^class ArchivedScore" questions/models.py -A30

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Find where Score model is defined
find . -name "*.py" -type f | xargs grep -l "^class Score" 2>/dev/null

Repository: Metaculus/metaculus

Length of output: 125


🏁 Script executed:

#!/bin/bash
# Look in scoring app for Score model
rg -n "class Score\|class ArchivedScore" scoring/ --type=py -A25

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check coherence/views.py and scoring/views.py for how user_scores is annotated
cat coherence/views.py | head -150

Repository: Metaculus/metaculus

Length of output: 5048


🏁 Script executed:

#!/bin/bash
# Get Score model definition
sed -n '1,300p' scoring/models.py | grep -n "class Score\|class ArchivedScore" -A30 | head -80

Repository: Metaculus/metaculus

Length of output: 2199


🏁 Script executed:

#!/bin/bash
# Look at the entire Score model
rg -n "class Score" scoring/models.py -A50 | head -100

Repository: Metaculus/metaculus

Length of output: 1800


🏁 Script executed:

#!/bin/bash
# Check if there's any resolution filtering when scores are created/queried
rg -n "resolution\|ANNULLED\|isUnsuccessfullyResolved" scoring/models.py -B3 -A3

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Search for where scores are created/calculated and if there's any resolution check
rg -n "Score.objects.create\|Score.objects.bulk_create\|\.scores\.all\|\.user_scores" scoring/ --type=py -B5 -A5 | head -150

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for task/job that calculates scores - might have resolution filtering
rg -n "def.*score\|@.*task\|@.*job" scoring/tasks.py -B3 -A20 | head -200

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if there's any lifecycle event handler when resolution changes
rg -n "resolution.*=\|ANNULLED\|post_save" questions/models.py -B3 -A3

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Search broadly for where Score objects are saved
rg -n "Score(" scoring/ --type=py -B5 -A5 | head -150

Repository: Metaculus/metaculus

Length of output: 126


🏁 Script executed:

#!/bin/bash
# Check the frontend code to see how shouldQuestionShowUserScores is used
grep -r "shouldQuestionShowUserScores" front_end/src --include="*.ts" --include="*.tsx" -B3 -A3

Repository: Metaculus/metaculus

Length of output: 1228


🏁 Script executed:

#!/bin/bash
# Look at tests to understand the expected behavior
find . -name "*test*.py" -type f | xargs grep -l "shouldQuestionShowUserScores\|score_data" 2>/dev/null | head -5

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if there's documentation about when scores are populated
rg -n "score_data\|my_forecasts" . --type=md -B2 -A2

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look at shouldQuestionShowScores to understand the difference
grep -r "shouldQuestionShowScores\|isUnsuccessfullyResolved" front_end/src -B5 -A5

Repository: Metaculus/metaculus

Length of output: 32723


🏁 Script executed:

#!/bin/bash
# Check what isUnsuccessfullyResolved does
grep -r "isUnsuccessfullyResolved\|unsuccessful.*resolution" front_end/src --include="*.ts" --include="*.tsx" -B3 -A10

Repository: Metaculus/metaculus

Length of output: 38297


🏁 Script executed:

#!/bin/bash
# See the full utils.ts file to compare both functions
cat front_end/src/app/\(main\)/questions/\[id\]/components/post_score_data/utils.ts

Repository: Metaculus/metaculus

Length of output: 1706


shouldQuestionShowUserScores is missing the unsuccessful resolution guard

shouldQuestionShowScores explicitly checks !isUnsuccessfullyResolved(question.resolution), but shouldQuestionShowUserScores omits this guard. The backend serializes my_forecasts.score_data directly from stored scores without filtering by resolution status. If the backend ever populates scores for annulled or ambiguous questions, the "My Scores" tab would unexpectedly appear. Add the same guard for consistency and defensiveness.

Suggested fix (consistent with CP variant)
 export const shouldQuestionShowUserScores = (
   question: QuestionWithForecasts
 ) => {
   const userScores = question.my_forecasts?.score_data;
-  return !isNil(userScores) && Object.keys(userScores).length > 0;
+  return (
+    !isNil(userScores) &&
+    Object.keys(userScores).length > 0 &&
+    !isUnsuccessfullyResolved(question.resolution)
+  );
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/questions/[id]/components/post_score_data/utils.ts
around lines 23 - 28, shouldQuestionShowUserScores currently returns true
whenever question.my_forecasts?.score_data exists, but it lacks the
unsuccessful-resolution guard present in shouldQuestionShowScores; update
shouldQuestionShowUserScores to also return false when
isUnsuccessfullyResolved(question.resolution) is true (i.e., add a check like
!isUnsuccessfullyResolved(question.resolution) combined with the existing
user_scores check), referencing the same question.my_forecasts?.score_data and
isUnsuccessfullyResolved symbol to ensure the "My Scores" tab stays hidden for
annulled/ambiguous resolutions.

Comment on lines +95 to +99
const defaultValue: TabKey = "comments";
const active =
activeTab && tabs.some((tab) => tab.key === activeTab)
? activeTab
: defaultValue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tab bar selection and panel content go out of sync when the active tab is removed from the list

active is used only as the value prop for the <Tabs> UI — it is never written back to context. When a tab (e.g., "similar-questions") is dropped from the computed list because isSm flips to true, the tab bar correctly shows "comments", but activeTab in QuestionLayoutContext still holds "similar-questions". Because renderActivePanel in tabs.tsx reads directly from the context, the panel renders <SimilarQuestionsTab> while the tab bar highlights "comments".

Add a useEffect to sync the context when a fallback is triggered:

🐛 Proposed fix
+ import { useEffect } from "react";
  ...

  const defaultValue: TabKey = "comments";
  const active =
    activeTab && tabs.some((tab) => tab.key === activeTab)
      ? activeTab
      : defaultValue;

+ useEffect(() => {
+   if (activeTab && !tabs.some((tab) => tab.key === activeTab)) {
+     setActiveTab(defaultValue);
+   }
+   // Deliberately depending on the variables that control tab availability,
+   // not on `tabs` itself (new array ref every render).
+ }, [activeTab, isSm, hasScores, hasSimilarQuestionsTab, setActiveTab]);

  return (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const defaultValue: TabKey = "comments";
const active =
activeTab && tabs.some((tab) => tab.key === activeTab)
? activeTab
: defaultValue;
import { useEffect } from "react";
const defaultValue: TabKey = "comments";
const active =
activeTab && tabs.some((tab) => tab.key === activeTab)
? activeTab
: defaultValue;
useEffect(() => {
if (activeTab && !tabs.some((tab) => tab.key === activeTab)) {
setActiveTab(defaultValue);
}
// Deliberately depending on the variables that control tab availability,
// not on `tabs` itself (new array ref every render).
}, [activeTab, isSm, hasScores, hasSimilarQuestionsTab, setActiveTab]);
return (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/questions/[id]/components/question_page_shell/tab_bar.tsx
around lines 95 - 99, The UI shows a different highlighted tab than the panel
because the local `active` fallback in tab_bar.tsx is not syncing back to the
layout context when the current `activeTab` is removed; add a useEffect in the
TabBar component that watches `active` and `activeTab`/`tabs` and, when
`activeTab` is not present in `tabs`, calls the context updater (e.g.,
`setActiveTab`) to set the context to the computed fallback `active` so
`renderActivePanel` in tabs.tsx reads the same value the <Tabs> control
displays.

Comment on lines +26 to +29
} else if (
withCursor ? x > chartWidth - estimatedTextWidth : x > chartWidth - 12
) {
textAnchor = "end";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Asymmetric right-boundary check may leave wide labels unanchored when withCursor is false.

The two branches use different thresholds:

  • withCursor: x > chartWidth - estimatedTextWidth — correct half-width-aware guard.
  • !withCursor: x > chartWidth - 12 — fixed 12 px regardless of text width.

For a label with estimatedTextWidth > 12, a tick sitting within [chartWidth - estimatedTextWidth, chartWidth - 12] will not flip to "end", so the label's right edge overflows the chart boundary while staying "middle"-anchored. Example: chartWidth=500, x=470, estimatedTextWidth=40 → right edge at 510, but 470 > 488 is false → stays "middle".

If the intent is to switch to "end" only when the tick itself is within 12 px of the edge (positional, not text-aware), the left-boundary guard should be made symmetrically positional too. If the intent is to mirror the cursor case, the fix is straightforward:

🐛 Proposed fix
  } else if (
-   withCursor ? x > chartWidth - estimatedTextWidth : x > chartWidth - 12
+   x > chartWidth - estimatedTextWidth
  ) {

Or, if the 12 px threshold is deliberately tighter for the no-cursor layout, document the intent with a comment so reviewers understand why the two cases differ.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (
withCursor ? x > chartWidth - estimatedTextWidth : x > chartWidth - 12
) {
textAnchor = "end";
} else if (
x > chartWidth - estimatedTextWidth
) {
textAnchor = "end";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/components/charts/primitives/x_tick_label.tsx` around lines 26
- 29, The right-boundary check is asymmetric: when withCursor is false the code
uses a fixed 12px threshold causing wide labels (estimatedTextWidth > 12) to
overflow; in the conditional around x/chartWidth replace the ternary so both
branches use a half-width-aware guard (e.g. x > chartWidth - estimatedTextWidth)
or, if you deliberately want a positional 12px guard when withCursor is false,
add a clarifying comment explaining why the thresholds differ; update the
expression that sets textAnchor (the block referencing withCursor, x,
chartWidth, estimatedTextWidth, and textAnchor) accordingly.

Comment thread front_end/src/components/comment_feed/comment.tsx
Comment on lines +348 to +351
const showBotPrivacyToggle = !profileId && !!user?.is_bot;
const showWelcomePrompt = !!postId && showWelcomeMessage && !user?.is_bot;
const showCommentHeader =
!compactVersion && (showTitle || showBotPrivacyToggle || showWelcomePrompt);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all usages of CommentFeed with compactVersion prop to understand callers
rg -n "compactVersion" --type=tsx -A2 -B2
rg -n "compactVersion" -A2 -B2

Repository: Metaculus/metaculus

Length of output: 6281


🏁 Script executed:

sed -n '410,440p' front_end/src/components/comment_feed/index.tsx

Repository: Metaculus/metaculus

Length of output: 1376


🏁 Script executed:

sed -n '400,420p' front_end/src/components/comment_feed/index.tsx

Repository: Metaculus/metaculus

Length of output: 930


🏁 Script executed:

sed -n '348,365p' front_end/src/components/comment_feed/index.tsx

Repository: Metaculus/metaculus

Length of output: 671


🏁 Script executed:

sed -n '365,415p' front_end/src/components/comment_feed/index.tsx

Repository: Metaculus/metaculus

Length of output: 2076


Sort/count display is unconditionally rendered, bypassing the compactVersion gate.

showCommentHeader correctly includes !compactVersion (line 351), hiding the title/bot-toggle/welcome-message block in compact mode. However, the sort label + comment count row (lines 413–437) sits outside that conditional and renders unconditionally — including when compactVersion={true}. For callers like ResponsiveCommentFeed that expect compact mode to suppress UI elements on small screens, this is a regression.

Fix: Gate the sort/count row on !compactVersion:

+        {!compactVersion && (
         <div className="mb-4 flex flex-row items-center justify-start gap-1 md:mb-5">
           <span ...>
             ...
           </span>
           <DropdownMenu ...>
             ...
           </DropdownMenu>
         </div>
+        )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/components/comment_feed/index.tsx` around lines 348 - 351, The
sort/count row is rendering regardless of compact mode; wrap or gate the
sort-and-count UI using the same compactVersion check as showCommentHeader
(i.e., ensure the sort/count block is only rendered when !compactVersion).
Locate the sort/count JSX near the comment header rendering (references:
showCommentHeader, compactVersion) and move it inside the same conditional or
add a condition like !compactVersion so callers such as ResponsiveCommentFeed
receive a true compact view with those elements suppressed.

variant="text"
className={cn(
"border-nonepx-2 h-6 gap-1 text-nowrap rounded-xs border-none py-1 text-xs font-normal text-gray-700 dark:text-gray-700-dark md:gap-2 md:px-2.5",
"border-nonepx-2 h-6 gap-1 text-nowrap rounded-xs border-none py-1 text-justify text-sm font-normal leading-4 text-gray-700 dark:text-gray-700-dark md:gap-2 md:px-2.5",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Typo merges border-none and px-2 into an invalid Tailwind token.

The string "border-nonepx-2 ..." is missing a space between border-none and px-2. Tailwind will silently ignore the unknown class border-nonepx-2, so px-2 (horizontal padding) is never applied. Additionally, border-none is already present later in the same string, making that half of the fused token a duplicate.

🐛 Proposed fix
-        "border-nonepx-2 h-6 gap-1 text-nowrap rounded-xs border-none py-1 text-justify text-sm font-normal leading-4 text-gray-700 dark:text-gray-700-dark md:gap-2 md:px-2.5",
+        "h-6 gap-1 px-2 text-nowrap rounded-xs border-none py-1 text-justify text-sm font-normal leading-4 text-gray-700 dark:text-gray-700-dark md:gap-2 md:px-2.5",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"border-nonepx-2 h-6 gap-1 text-nowrap rounded-xs border-none py-1 text-justify text-sm font-normal leading-4 text-gray-700 dark:text-gray-700-dark md:gap-2 md:px-2.5",
"h-6 gap-1 px-2 text-nowrap rounded-xs border-none py-1 text-justify text-sm font-normal leading-4 text-gray-700 dark:text-gray-700-dark md:gap-2 md:px-2.5",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/components/post_card/basic_post_card/comment_status.tsx` at
line 57, The className string contains a merged token "border-nonepx-2" which
should be split and de-duplicated; update the string so it contains "border-none
px-2" (remove the duplicate extra "border-none" if present elsewhere in that
same string) so Tailwind correctly applies the px-2 horizontal padding; locate
and fix the literal class string in comment_status.tsx where "border-nonepx-2
h-6..." appears.

Comment on lines +80 to 88
{vote.score != null && vote.score !== 0 && (
<span
className={cn(
"font-normal leading-4 text-gray-700 dark:text-gray-700-dark md:text-sm",
compact ? "text-xs" : "text-sm"
)}
>
<span className="lining-nums tabular-nums">{vote.score}</span>
</span>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify what cn() is backed by (clsx-only vs tailwind-merge)
fd -e ts -e tsx -i "cn.ts" --exec cat {}

# Also check if tailwind-merge is used in the cn utility
rg -n "tailwind-merge\|twMerge\|clsx" --type=ts

Repository: Metaculus/metaculus

Length of output: 231


compact=true has no effect at md+ breakpoints due to conflicting md:text-sm base class

When compact=true, the class string becomes md:text-sm text-xs. Since cn() uses tailwind-merge, responsive variants like md:text-sm override base utilities at their breakpoint. This means text-xs applies only on mobile (<768px), but on desktop (≥768px) the md:text-sm always wins, rendering text-sm regardless. Since compact is intended for the SimilarQuestionCard sidebar (desktop context), the compact styling is never actually applied where needed.

🐛 Proposed fix
-      <span
-        className={cn(
-          "font-normal leading-4 text-gray-700 dark:text-gray-700-dark md:text-sm",
-          compact ? "text-xs" : "text-sm"
-        )}
-      >
+      <span
+        className={cn(
+          "font-normal leading-4 text-gray-700 dark:text-gray-700-dark",
+          compact ? "text-xs" : "text-sm"
+        )}
+      >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{vote.score != null && vote.score !== 0 && (
<span
className={cn(
"font-normal leading-4 text-gray-700 dark:text-gray-700-dark md:text-sm",
compact ? "text-xs" : "text-sm"
)}
>
<span className="lining-nums tabular-nums">{vote.score}</span>
</span>
{vote.score != null && vote.score !== 0 && (
<span
className={cn(
"font-normal leading-4 text-gray-700 dark:text-gray-700-dark",
compact ? "text-xs" : "text-sm"
)}
>
<span className="lining-nums tabular-nums">{vote.score}</span>
</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/components/post_card/basic_post_card/post_voter.tsx` around
lines 80 - 88, The issue is the hardcoded "md:text-sm" in the static class
string overriding the compact branch at desktop; update the class logic in
post_voter.tsx so responsive sizing can be controlled by the compact prop:
remove the fixed "md:text-sm" and use a non-responsive "text-sm" as the default,
then let the compact conditional (compact ? "text-xs" : "text-sm") determine
sizing (or, alternatively, move "md:text-sm" into the non-compact branch).
Adjust the cn(...) call around the vote score span accordingly so compact=true
wins at md+.

Comment thread posts/serializers.py
Comment on lines +506 to 507
if current_user and include_user_forecasts:
qs = qs.prefetch_user_forecasts(current_user.id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

include_user_forecasts silently has no effect when with_cp=False

The prefetch is nested inside the if with_cp: block, so callers that pass include_user_forecasts=True without also ensuring with_cp=True (e.g., posts_list_api_view where with_cp comes from an optional query param and defaults to None) will get no user-forecast data without any indication. At minimum, document the dependency in the function signature; a stricter option is an assertion.

🛡️ Suggested defensive guard
+    if include_user_forecasts and not with_cp:
+        raise ValueError("include_user_forecasts requires with_cp=True")
     if with_cp:
         qs = qs.prefetch_questions_scores()

         if current_user and include_user_forecasts:
             qs = qs.prefetch_user_forecasts(current_user.id)

Or, as a softer option, move the prefetch outside the with_cp guard (if user forecasts are needed independently of CP data).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@posts/serializers.py` around lines 506 - 507, The prefetch of user forecasts
is currently nested under the with_cp conditional so include_user_forecasts=True
can silently do nothing; either document this dependency in the function
signature or enforce it — e.g., add an assertion that include_user_forecasts
implies with_cp, or move the call to qs.prefetch_user_forecasts(current_user.id)
out of the with_cp block so include_user_forecasts takes effect independently;
update the relevant function signature/docstring and callers (e.g.,
posts_list_api_view) accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Question Page Redesign – Page shell

1 participant