Skip to content

feat/add-entry-exclusion-subtlety#4206

Open
lsabor wants to merge 4 commits intomainfrom
feat/add-entry-exclusion-subtlety
Open

feat/add-entry-exclusion-subtlety#4206
lsabor wants to merge 4 commits intomainfrom
feat/add-entry-exclusion-subtlety

Conversation

@lsabor
Copy link
Contributor

@lsabor lsabor commented Jan 31, 2026

new settings allow for fine tuned display rules for leaderboard entries
add exclusion_status enum ExclusionStatuses to LeaderboardEntry and MedalExclusionRecord
set up to deprecate excluded and show_when_excluded fields

Summary by CodeRabbit

  • Refactor
    • Modernized leaderboard entry visibility and exclusion management with a new status-based system providing fine-grained control across different user roles and viewing contexts.
    • Enhanced display logic for entries in standard and advanced viewing modes with improved staff access controls.
    • Updated leaderboard calculations and filtering logic to support more sophisticated visibility rules.

✏️ Tip: You can customize this high-level summary in your review settings.

add exclusion_status enum 'ExclusionStatuses' to LeaderboardEntry and MedalExclusionRecord
new settings allow for fine tuned display rules
set up to depricate excluded and show_when_excluded fields
@lsabor lsabor marked this pull request as draft January 31, 2026 17:34
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 31, 2026

📝 Walkthrough

Walkthrough

This PR consolidates leaderboard entry exclusion logic by replacing two separate boolean fields (excluded and show_when_excluded) with a single enumeration field (exclusion_status using ExclusionStatuses). The refactor spans frontend components, type definitions, backend models, migrations, serializers, utilities, and views to implement a unified status-based visibility control system.

Changes

Cohort / File(s) Summary
Frontend Leaderboard Components
front_end/src/app/(main)/(leaderboards)/leaderboard/components/leaderboard_table/index.tsx, front_end/src/app/(main)/(leaderboards)/leaderboard/components/leaderboard_table/table_row.tsx, front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/index.tsx, front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/table_row.tsx
Replaced boolean exclusion checks with ExclusionStatuses enum-based filtering. Updated row visibility and tooltip rendering logic to check exclusion_status values and staff privileges instead of simple boolean flags.
Type and Model Definitions
front_end/src/types/scoring.ts, scoring/models.py
Introduced new ExclusionStatuses enum with multiple status values (INCLUDE, EXCLUDE_PRIZE_AND_SHOW, EXCLUDE_AND_SHOW, EXCLUDE_AND_SHOW_IN_ADVANCED, EXCLUDE). Updated LeaderboardEntry and MedalExclusionRecord types to use exclusion_status field instead of excluded and show_when_excluded booleans. Added new fields: contribution_count, calculated_on, take, and percent_prize.
Database Migration
scoring/migrations/0020_leaderboardentry_and_medalexclusionrecord_exclusion_status.py
Data migration that adds exclusion_status field to LeaderboardEntry and MedalExclusionRecord models. Transforms existing boolean exclusion states into enumeration values using Case/When expressions. Includes reverse migration to reconstruct booleans from enum values.
Backend Serialization and Admin
scoring/serializers.py, scoring/admin.py
Added exclusion_status field to LeaderboardEntrySerializer for API exposure. Updated admin list display to show exclusion_status column instead of excluded. Retained deprecated excluded field in serializer with documentation note.
Backend Utilities and Views
scoring/utils.py, scoring/views.py, scoring/management/commands/update_global_bot_leaderboard.py
Refactored exclusion logic throughout utility functions, leaderboard ranking, prize calculation, medal allocation, and CSV import/export to use ExclusionStatuses enum. Updated query filters and ranking calculations to check exclusion_status values. Simplified non-staff leaderboard filtering with uniform status-based gates.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Metaculus/metaculus#4207: Modifies leaderboard visibility filtering in scoring/views.py using the previous excluded/show_when_excluded flags; this PR replaces that logic with the new exclusion_status enum and corresponding filters.

Poem

🐰 Hop hop, the booleans take their bow,
An enum hops in to show us how,
One status field, so clean and bright,
Exclusion logic now feels just right!

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'feat/add-entry-exclusion-subtlety' is vague and uses non-descriptive language that doesn't clearly convey what the changeset accomplishes. Consider a more specific title like 'refactor: replace boolean exclusion flags with ExclusionStatuses enum' or 'feat: add exclusion_status field with granular display rules for leaderboard entries' to better describe the main change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/add-entry-exclusion-subtlety

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 31, 2026

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4206-feat-add-entry-exclusion-subtl-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:feat-add-entry-exclusion-subtlety-b8c3c83
🗄️ PostgreSQL NeonDB branch preview/pr-4206-feat-add-entry-exclusion-subtl
Redis Fly Redis mtc-redis-pr-4206-feat-add-entry-exclusion-subtl

Details

  • Commit: b8c3c83a3b79c534e3627c3ce5ef42a3cda3fa66
  • Branch: feat/add-entry-exclusion-subtlety
  • Fly App: metaculus-pr-4206-feat-add-entry-exclusion-subtl

ℹ️ 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

Copy link
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: 1

Caution

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

⚠️ Outside diff range comments (5)
front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/table_row.tsx (1)

42-80: ⚠️ Potential issue | 🟠 Major

Show rank for EXCLUDE_PRIZE_AND_SHOW entries.

Status (1) is supposed to take rank, but the current condition hides the rank for any non-INCLUDE entry, so prize-excluded entries lose their rank display.

🐛 Suggested fix
   const t = useTranslations();
+  const hidesRank =
+    exclusion_status === ExclusionStatuses.EXCLUDE_AND_SHOW ||
+    exclusion_status === ExclusionStatuses.EXCLUDE_AND_SHOW_IN_ADVANCED ||
+    exclusion_status === ExclusionStatuses.EXCLUDE;
   const highlight =
     user?.id === userId || exclusion_status !== ExclusionStatuses.INCLUDE;
@@
-              {exclusion_status !== ExclusionStatuses.INCLUDE ? (
+              {hidesRank ? (
                 <>
                   <ExcludedEntryTooltip />
                 </>
               ) : (
front_end/src/app/(main)/(leaderboards)/leaderboard/components/leaderboard_table/table_row.tsx (1)

70-96: ⚠️ Potential issue | 🟠 Major

Show rank for EXCLUDE_PRIZE_AND_SHOW entries.

Status (1) should still take rank, but the current condition hides the rank for any non-INCLUDE entry.

🐛 Suggested fix
   const t = useTranslations();
+  const hidesRank =
+    exclusion_status === ExclusionStatuses.EXCLUDE_AND_SHOW ||
+    exclusion_status === ExclusionStatuses.EXCLUDE_AND_SHOW_IN_ADVANCED ||
+    exclusion_status === ExclusionStatuses.EXCLUDE;
@@
-                {exclusion_status != ExclusionStatuses.INCLUDE ? (
+                {hidesRank ? (
                   <>
                     <ExcludedEntryTooltip />
                   </>
                 ) : (
scoring/views.py (1)

250-255: ⚠️ Potential issue | 🟡 Minor

Total entries should align with rank‑eligible statuses.

total_entries currently counts entries up to EXCLUDE_AND_SHOW_IN_ADVANCED, which includes rows excluded from ranks. This can inflate totals. Consider counting only rank‑eligible statuses.

🔧 Suggested fix
-                exclusion_status__lte=ExclusionStatuses.EXCLUDE_AND_SHOW_IN_ADVANCED,
+                exclusion_status__lte=ExclusionStatuses.EXCLUDE_PRIZE_AND_SHOW,
scoring/utils.py (1)

860-875: ⚠️ Potential issue | 🟡 Minor

Normalize exclusion_status when importing CSV.

row.get returns strings/empties; passing these directly to an IntegerField can raise or store unexpected values. Consider converting to int with a safe default.

🔧 Suggested fix
-        exclusion_status = row.get("exclusion_status")
+        exclusion_status = row.get("exclusion_status")
+        exclusion_status = (
+            int(exclusion_status)
+            if exclusion_status not in (None, "")
+            else ExclusionStatuses.INCLUDE
+        )
...
-            exclusion_status=exclusion_status,
+            exclusion_status=exclusion_status,
scoring/management/commands/update_global_bot_leaderboard.py (1)

760-768: ⚠️ Potential issue | 🔴 Critical

Command must update deprecated excluded field to maintain consistency with exclusion_status.

The command only sets exclusion_status but leaves excluded and show_when_excluded at their default values (both False). At runtime, scoring/utils.py:667 filters entries by excluded=False to calculate leaderboard statistics. Entries marked exclusion_status=EXCLUDE will still have excluded=False, causing them to be incorrectly included in stats calculations. Either update both the deprecated fields here to match the exclusion_status logic, or refactor the statistics query to check exclusion_status instead of the deprecated fields.

🤖 Fix all issues with AI agents
In `@scoring/views.py`:
- Around line 71-78: The rank threshold calculation uses
entries.filter(exclusion_status=ExclusionStatuses.INCLUDE).count(), which omits
entries like ExclusionStatuses.EXCLUDE_PRIZE_AND_SHOW that still consume ranks;
update the count to include all rank-eligible statuses (e.g., replace the
.filter(...) call with a filter that includes both ExclusionStatuses.INCLUDE and
ExclusionStatuses.EXCLUDE_PRIZE_AND_SHOW or the inverse by excluding only
non-rank-eligible statuses), so the max(... * 0.05) uses the correct total;
adjust the expression around rank__lte=max(...) accordingly.
🧹 Nitpick comments (2)
scoring/models.py (1)

345-355: Consider indexing exclusion_status to replace the old excluded index.

If queries now filter on exclusion_status, the previous db_index on excluded won’t help and could introduce a perf regression. Adding an index (and migration) keeps filtering fast.

♻️ Suggested adjustment
-    exclusion_status: ExclusionStatuses = models.IntegerField(
-        max_length=200,
+    exclusion_status: ExclusionStatuses = models.IntegerField(
+        db_index=True,
+        max_length=200,
         choices=ExclusionStatuses.choices,
         default=ExclusionStatuses.INCLUDE,
         help_text="""This sets the exclusion status of this entry.
front_end/src/types/scoring.ts (1)

52-68: Prefer ExclusionStatuses for exclusion_status type safety.

Now that the enum exists, using it directly prevents invalid values and keeps the type aligned with backend semantics.

♻️ Suggested refactor
-  exclusion_status: number;
+  exclusion_status: ExclusionStatuses;
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8040205 and 1a7a0fb.

📒 Files selected for processing (12)
  • front_end/src/app/(main)/(leaderboards)/leaderboard/components/leaderboard_table/index.tsx
  • front_end/src/app/(main)/(leaderboards)/leaderboard/components/leaderboard_table/table_row.tsx
  • front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/index.tsx
  • front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/table_row.tsx
  • front_end/src/types/scoring.ts
  • scoring/admin.py
  • scoring/management/commands/update_global_bot_leaderboard.py
  • scoring/migrations/0020_leaderboardentry_and_medalexclusionrecord_exclusion_status.py
  • scoring/models.py
  • scoring/serializers.py
  • scoring/utils.py
  • scoring/views.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-20T16:49:51.584Z
Learnt from: aseckin
Repo: Metaculus/metaculus PR: 4001
File: front_end/src/app/(futureeval)/futureeval/components/futureeval-leaderboard-table.tsx:15-22
Timestamp: 2026-01-20T16:49:51.584Z
Learning: The mockTranslate function in front_end/src/app/(futureeval)/futureeval/components/futureeval-leaderboard-table.tsx is an intentional workaround to satisfy the entryLabel function signature from shared AIB utils while keeping FutureEval translation-free.

Applied to files:

  • front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/table_row.tsx
  • front_end/src/app/(main)/(leaderboards)/leaderboard/components/leaderboard_table/index.tsx
  • front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/index.tsx
  • front_end/src/app/(main)/(leaderboards)/leaderboard/components/leaderboard_table/table_row.tsx
📚 Learning: 2026-01-15T19:29:58.940Z
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4075
File: authentication/urls.py:24-26
Timestamp: 2026-01-15T19:29:58.940Z
Learning: In this codebase, DRF is configured to use IsAuthenticated as the default in REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] within metaculus_web/settings.py. Therefore, explicit permission_classes([IsAuthenticated]) decorators are unnecessary on DRF views unless a view needs to override the default. When reviewing Python files, verify that views relying on the default are not redundantly decorated, and flag cases where permissions are being over-specified or when a non-default permission is explicitly required.

Applied to files:

  • scoring/models.py
  • scoring/admin.py
  • scoring/serializers.py
  • scoring/migrations/0020_leaderboardentry_and_medalexclusionrecord_exclusion_status.py
  • scoring/management/commands/update_global_bot_leaderboard.py
  • scoring/utils.py
  • scoring/views.py
🧬 Code graph analysis (10)
front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/table_row.tsx (1)
scoring/models.py (1)
  • ExclusionStatuses (17-22)
front_end/src/app/(main)/(leaderboards)/leaderboard/components/leaderboard_table/index.tsx (1)
scoring/models.py (1)
  • ExclusionStatuses (17-22)
scoring/models.py (1)
projects/permissions.py (1)
  • choices (10-15)
front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/index.tsx (1)
scoring/models.py (1)
  • ExclusionStatuses (17-22)
scoring/migrations/0020_leaderboardentry_and_medalexclusionrecord_exclusion_status.py (1)
scoring/models.py (2)
  • LeaderboardEntry (314-380)
  • MedalExclusionRecord (410-495)
scoring/management/commands/update_global_bot_leaderboard.py (2)
scoring/models.py (3)
  • Leaderboard (105-292)
  • LeaderboardEntry (314-380)
  • ExclusionStatuses (17-22)
front_end/src/types/scoring.ts (1)
  • LeaderboardEntry (60-75)
front_end/src/app/(main)/(leaderboards)/leaderboard/components/leaderboard_table/table_row.tsx (1)
scoring/models.py (1)
  • ExclusionStatuses (17-22)
scoring/utils.py (2)
scoring/models.py (1)
  • ExclusionStatuses (17-22)
projects/models.py (1)
  • BotLeaderboardStatus (248-252)
front_end/src/types/scoring.ts (1)
scoring/models.py (2)
  • ExclusionStatuses (17-22)
  • LeaderboardEntry (314-380)
scoring/views.py (2)
scoring/models.py (3)
  • Leaderboard (105-292)
  • LeaderboardEntry (314-380)
  • ExclusionStatuses (17-22)
front_end/src/types/scoring.ts (1)
  • LeaderboardEntry (60-75)
🪛 Ruff (0.14.14)
scoring/admin.py

[warning] 156-156: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

scoring/migrations/0020_leaderboardentry_and_medalexclusionrecord_exclusion_status.py

[warning] 7-7: Unused function argument: schema_editor

(ARG001)


[warning] 27-27: Unused function argument: schema_editor

(ARG001)


[warning] 56-58: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 60-104: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build Docker Image / Build Docker Image
🔇 Additional comments (11)
scoring/models.py (3)

17-22: ExclusionStatuses enum looks good.

Clear mapping and explicit values make the new status system easy to reason about.


193-196: No review notes for this TODO block.


450-460: LGTM: exclusion_status added to MedalExclusionRecord.

This aligns with the shared enum and keeps defaults explicit.

scoring/admin.py (1)

156-156: LGTM: admin list_display now surfaces exclusion_status.

scoring/serializers.py (1)

15-40: LGTM: serializer now exposes exclusion_status alongside legacy fields.

front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/index.tsx (1)

107-123: LGTM: status-based filtering matches the new enum semantics.

front_end/src/app/(main)/(leaderboards)/leaderboard/components/leaderboard_table/index.tsx (1)

92-103: LGTM: exclusion-status gating is clear and consistent.

scoring/views.py (1)

84-87: Advanced-only entries may be exposed to standard users.

EXCLUDE_AND_SHOW_IN_ADVANCED is included for all non‑staff. If this endpoint backs the standard leaderboard view, advanced‑only entries will leak. Please confirm the intended audience and gate this filter on an explicit “advanced” flag if needed.

scoring/migrations/0020_leaderboardentry_and_medalexclusionrecord_exclusion_status.py (1)

7-24: Migration + backfill mapping looks consistent.

Forward and reverse mappings align with the deprecated boolean semantics and provide a clear downgrade path.

Also applies to: 27-51, 60-103

scoring/utils.py (2)

43-44: Exclusion status propagation through ranks/prizes looks consistent.

Rank gating, prize pool participation, and prize assignment align with the new enum semantics.

Also applies to: 468-528, 741-742


545-553: Confirm medal eligibility for EXCLUDE_PRIZE_AND_SHOW.

Medals are currently limited to INCLUDE. If EXCLUDE_PRIZE_AND_SHOW entries should still be medal‑eligible (since they take rank), this will skip them.

🔧 Possible adjustment (if medal‑eligible)
-    entry_count = len(
-        [e for e in entries if e.exclusion_status == ExclusionStatuses.INCLUDE]
-    )
+    entry_count = len(
+        [
+            e
+            for e in entries
+            if e.exclusion_status <= ExclusionStatuses.EXCLUDE_PRIZE_AND_SHOW
+        ]
+    )
...
-        if entry.exclusion_status != ExclusionStatuses.INCLUDE:
+        if entry.exclusion_status > ExclusionStatuses.EXCLUDE_PRIZE_AND_SHOW:
             continue

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +71 to +78
rank__lte=max(
3,
np.ceil(
entries.filter(
exclusion_status=ExclusionStatuses.INCLUDE
).count()
* 0.05
),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Rank threshold should include rank‑eligible exclusions.

EXCLUDE_PRIZE_AND_SHOW entries still take rank, but the count only includes INCLUDE, which can undercount and shrink the top‑% window. Consider counting all rank‑eligible statuses.

🔧 Suggested fix
-                        entries.filter(
-                            exclusion_status=ExclusionStatuses.INCLUDE
-                        ).count()
+                        entries.filter(
+                            exclusion_status__lte=ExclusionStatuses.EXCLUDE_PRIZE_AND_SHOW
+                        ).count()
🤖 Prompt for AI Agents
In `@scoring/views.py` around lines 71 - 78, The rank threshold calculation uses
entries.filter(exclusion_status=ExclusionStatuses.INCLUDE).count(), which omits
entries like ExclusionStatuses.EXCLUDE_PRIZE_AND_SHOW that still consume ranks;
update the count to include all rank-eligible statuses (e.g., replace the
.filter(...) call with a filter that includes both ExclusionStatuses.INCLUDE and
ExclusionStatuses.EXCLUDE_PRIZE_AND_SHOW or the inverse by excluding only
non-rank-eligible statuses), so the max(... * 0.05) uses the correct total;
adjust the expression around rank__lte=max(...) accordingly.

Comment on lines +95 to 103
if (exclusionStatus == ExclusionStatuses.EXCLUDE) {
return null;
} else if (
exclusionStatus ==
ExclusionStatuses.EXCLUDE_AND_SHOW_IN_ADVANCED &&
!currentUser?.is_staff
) {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we merge these two?

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.

2 participants