Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b6cf2b8
feat/add-entry-exclusion-subtlety
lsabor Jan 31, 2026
92036da
undo lock update
lsabor Jan 31, 2026
0d2b02e
remove eslint new dependency
lsabor Jan 31, 2026
1a7a0fb
fix formatting
lsabor Jan 31, 2026
9d789c3
Merge branch 'main' of github.com:Metaculus/metaculus into feat/add-e…
lsabor Feb 14, 2026
ed09144
fix migration conflict
lsabor Feb 14, 2026
52b9321
edit help text
lsabor Feb 14, 2026
59d10e1
fix bugs, address comments
lsabor Feb 14, 2026
056359c
address more coments
lsabor Feb 14, 2026
85662a6
remove unused return
lsabor Feb 14, 2026
4592ebc
sp deprecate
lsabor Feb 14, 2026
6a6f839
Merge branch 'main' of github.com:Metaculus/metaculus into feat/add-e…
lsabor Feb 21, 2026
4c35d14
change implementation overall
lsabor Feb 21, 2026
5572736
admin improvements
lsabor Feb 21, 2026
9420c8b
address comments
lsabor Feb 21, 2026
4e8ec6a
Merge branch 'main' into feat/add-entry-exclusion-subtlety
cemreinanc Feb 22, 2026
e8fdf7c
support bot_status assignment in migration, order leaderboard entries…
lsabor Feb 22, 2026
58d2bb5
fix failing update F reference
lsabor Feb 22, 2026
4a701fd
remove unused import
lsabor Feb 22, 2026
b3bb851
Merge branch 'main' of github.com:Metaculus/metaculus into feat/add-e…
lsabor Mar 15, 2026
5f2101b
resolve merge conflicts, address two minor comments
lsabor Mar 15, 2026
3728aca
exclude prize only takes rank
lsabor Mar 15, 2026
c0b63b7
Merge branch 'main' of github.com:Metaculus/metaculus into feat/add-e…
lsabor Mar 21, 2026
53688fe
Merge branch 'main' of github.com:Metaculus/metaculus into feat/add-e…
lsabor Mar 21, 2026
96bcaea
improve inline exclusion handling features
lsabor Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { FC } from "react";

import { useAuth } from "@/contexts/auth_context";
import { useBreakpoint } from "@/hooks/tailwind";
import { CategoryKey, LeaderboardDetails } from "@/types/scoring";
import {
CategoryKey,
ExclusionStatuses,
LeaderboardDetails,
} from "@/types/scoring";

import LeaderboardRow, { UserLeaderboardRow } from "./table_row";
import { RANKING_CATEGORIES } from "../../../ranking_categories";
Expand Down Expand Up @@ -85,11 +89,15 @@ const LeaderboardTable: FC<Props> = ({
</tr>
{!!entriesToDisplay.length ? (
entriesToDisplay.map((entry) => {
// only show entries that are not excluded or if advanced mode is on
// or if the current user is staff
if (
entry.excluded &&
!(currentUser?.is_staff || entry.show_when_excluded)
// only show entries that are not excluded
// or if current user is staff and exclusion status allows showing in advanced mode
const exclusionStatus = entry.exclusion_status;
if (exclusionStatus == ExclusionStatuses.EXCLUDE) {
return null;
} else if (
exclusionStatus ==
ExclusionStatuses.EXCLUDE_AND_SHOW_IN_ADVANCED &&
!currentUser?.is_staff
) {
return null;
}
Comment thread
lsabor marked this conversation as resolved.
Outdated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FC } from "react";
import { Href } from "@/types/navigation";
import {
CategoryKey,
ExclusionStatuses,
LeaderboardEntry,
LeaderboardType,
} from "@/types/scoring";
Expand Down Expand Up @@ -52,7 +53,7 @@ const LeaderboardRow: FC<Props> = ({
contribution_count,
score,
medal,
excluded,
exclusion_status,
} = rowEntry;

const t = useTranslations();
Expand All @@ -67,7 +68,7 @@ const LeaderboardRow: FC<Props> = ({
},
{
"bg-purple-200 hover:bg-purple-300 dark:bg-purple-200-dark hover:dark:bg-purple-300-dark":
!isUserRow && excluded,
!isUserRow && exclusion_status != ExclusionStatuses.INCLUDE,
}
)}
>
Expand All @@ -85,7 +86,7 @@ const LeaderboardRow: FC<Props> = ({
<>
{!!medal && <MedalIcon type={medal} className="size-5" />}
<span className="flex-1 text-center">
{excluded ? (
{exclusion_status != ExclusionStatuses.INCLUDE ? (
<>
<ExcludedEntryTooltip />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { FC, useCallback, useMemo, useState } from "react";

import enMessages from "@/../messages/en.json";
import Button from "@/components/ui/button";
import { LeaderboardDetails, LeaderboardDisplayConfig } from "@/types/scoring";
import {
ExclusionStatuses,
LeaderboardDetails,
LeaderboardDisplayConfig,
} from "@/types/scoring";

import TableHeader from "./table_header";
import TableRow from "./table_row";
Expand Down Expand Up @@ -52,7 +56,11 @@ const ProjectLeaderboardTable: FC<Props> = ({

const filteredEntries = useMemo(() => {
return leaderboardDetails.entries.filter(
(entry) => !entry.excluded || entry.show_when_excluded || isAdvanced
(entry) =>
entry.exclusion_status <= ExclusionStatuses.EXCLUDE_AND_SHOW ||
(isAdvanced &&
entry.exclusion_status ==
ExclusionStatuses.EXCLUDE_AND_SHOW_IN_ADVANCED)
);
}, [leaderboardDetails.entries, isAdvanced]);

Expand Down Expand Up @@ -141,16 +149,26 @@ const ProjectLeaderboardTable: FC<Props> = ({
/>
)}
{leaderboardEntries.length > 0
? leaderboardEntries.map((entry) => (
<TableRow
key={entry.user?.id ?? entry.aggregation_method}
rowEntry={entry}
userId={userId}
maxCoverage={maxCoverage}
withPrizePool={!!leaderboardDetails.prize_pool}
isAdvanced={isAdvanced}
/>
))
? leaderboardEntries.map((entry) => {
if (
entry.exclusion_status <=
ExclusionStatuses.EXCLUDE_AND_SHOW ||
(isAdvanced &&
entry.exclusion_status ==
ExclusionStatuses.EXCLUDE_AND_SHOW_IN_ADVANCED)
) {
return (
<TableRow
key={entry.user?.id ?? entry.aggregation_method}
rowEntry={entry}
userId={userId}
maxCoverage={maxCoverage}
withPrizePool={!!leaderboardDetails.prize_pool}
isAdvanced={isAdvanced}
/>
);
}
})
Comment thread
lsabor marked this conversation as resolved.
Outdated
: !leaderboardDetails.userEntry && (
<tr className="border-b border-gray-300 dark:border-gray-300-dark">
<td
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link from "next/link";
import { useTranslations } from "next-intl";
import { FC, PropsWithChildren } from "react";

import { LeaderboardEntry } from "@/types/scoring";
import { ExclusionStatuses, LeaderboardEntry } from "@/types/scoring";
import cn from "@/utils/core/cn";
import { formatUsername } from "@/utils/formatters/users";

Expand Down Expand Up @@ -31,15 +31,16 @@ const TableRow: FC<Props> = ({
medal,
rank,
score,
excluded,
exclusion_status,
coverage,
contribution_count,
take,
percent_prize,
prize,
} = rowEntry;
const t = useTranslations();
const highlight = user?.id === userId || excluded;
const highlight =
user?.id === userId || exclusion_status !== ExclusionStatuses.INCLUDE;
const coveragePercent = coverage
? maxCoverage
? ((coverage / maxCoverage) * 100).toFixed(1) + "%"
Expand Down Expand Up @@ -70,7 +71,7 @@ const TableRow: FC<Props> = ({
)}

<span className="flex-1 text-center">
{excluded ? (
{exclusion_status !== ExclusionStatuses.INCLUDE ? (
<>
<ExcludedEntryTooltip />
</>
Expand Down
11 changes: 9 additions & 2 deletions front_end/src/types/scoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,22 @@ export type BotDetails = {
include_in_calculations?: boolean;
};

export enum ExclusionStatuses {
INCLUDE = 0,
EXCLUDE_PRIZE_AND_SHOW = 1,
EXCLUDE_AND_SHOW = 2,
EXCLUDE_AND_SHOW_IN_ADVANCED = 3,
EXCLUDE = 4,
}

export type LeaderboardEntry = {
user: (User & { metadata?: { bot_details: BotDetails } }) | null;
aggregation_method: string | null;
score: number;
rank: number | null;
ci_lower?: number;
ci_upper?: number;
excluded: boolean;
show_when_excluded: boolean;
exclusion_status: number;
medal: MedalType | null;
prize: number | null;
coverage: number;
Expand Down
2 changes: 1 addition & 1 deletion scoring/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class LeaderboardEntryAdmin(admin.ModelAdmin):
"leaderboard__project__slug",
"leaderboard__project__name_original",
]
list_display = ["__str__", "user", "rank", "score", "take", "excluded"]
list_display = ["__str__", "user", "rank", "score", "take", "exclusion_status"]
autocomplete_fields = ["leaderboard", "user"]
list_filter = [
AutocompleteFilterFactory("Leaderboard", "leaderboard"),
Expand Down
11 changes: 6 additions & 5 deletions scoring/management/commands/update_global_bot_leaderboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from questions.models import AggregateForecast, Forecast, Question
from questions.types import AggregationMethod
from scoring.constants import ScoreTypes, LeaderboardScoreTypes
from scoring.models import Leaderboard, LeaderboardEntry, Score
from scoring.models import Leaderboard, LeaderboardEntry, Score, ExclusionStatuses
from scoring.score_math import (
evaluate_forecasts_peer_accuracy,
evaluate_forecasts_peer_spot_forecast,
Expand Down Expand Up @@ -935,8 +935,8 @@ def run_update_global_bot_leaderboard(
excluded = False
if isinstance(uid, int):
user = User.objects.get(id=uid)
bot_details = (user.metadata or dict()).get("bot_details")
if bot_details and not bot_details.get("display_in_leaderboard"):
bot_details = user.metadata["bot_details"]
if not bot_details.get("display_in_leaderboard"):
excluded = True
Comment thread
lsabor marked this conversation as resolved.

entry: LeaderboardEntry = entry_dict.pop(uid, LeaderboardEntry())
Expand All @@ -945,8 +945,9 @@ def run_update_global_bot_leaderboard(
entry.leaderboard = leaderboard
entry.score = skill
entry.rank = rank
entry.excluded = excluded
entry.show_when_excluded = False
entry.exclusion_status = (
ExclusionStatuses.EXCLUDE if excluded else ExclusionStatuses.INCLUDE
)
entry.contribution_count = contribution_count
entry.coverage = contribution_count / question_count
entry.calculated_on = timezone.now()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Generated by Django 5.1.15 on 2026-01-31 15:26

from django.db import migrations, models
from django.db.models import BooleanField, Case, IntegerField, Value, When


def migrate(apps, schema_editor):
LeaderboardEntry = apps.get_model("scoring", "LeaderboardEntry")
LeaderboardEntry.objects.update(
exclusion_status=Case(
When(excluded=False, then=Value(0)), # "Include"
When(show_when_excluded=True, then=Value(2)), # "Exclude and Show"
default=Value(4), # "Exclude"
output_field=IntegerField(),
)
)
MedalExclusionRecord = apps.get_model("scoring", "MedalExclusionRecord")
MedalExclusionRecord.objects.update(
exclusion_status=Case(
When(show_anyway=True, then=Value(2)), # "Exclude and Show"
default=Value(4), # "Exclude"
output_field=IntegerField(),
)
)


def reverse_migrate(apps, schema_editor):
LeaderboardEntry = apps.get_model("scoring", "LeaderboardEntry")
# All values between include and exclude map to:
# excluded = True
# show_when_excluded = True
LeaderboardEntry.objects.update(
excluded=Case(
When(exclusion_status=0, then=Value(False)), # "Include"
default=Value(True),
output_field=BooleanField(),
),
show_when_excluded=Case(
When(exclusion_status=4, then=Value(False)), # "Exclude"
default=Value(True),
output_field=BooleanField(),
),
)
MedalExclusionRecord = apps.get_model("scoring", "MedalExclusionRecord")
MedalExclusionRecord.objects.update(
show_anyway=Case(
When(exclusion_status=4, then=Value(False)), # "Exclude"
default=Value(True),
output_field=BooleanField(),
),
)


class Migration(migrations.Migration):

dependencies = [
("scoring", "0020_leaderboard_display_config"),
]

operations = [
migrations.AddField(
model_name="leaderboardentry",
name="exclusion_status",
field=models.IntegerField(
choices=[
(0, "Include"),
(1, "Exclude Prize And Show"),
(2, "Exclude And Show"),
(3, "Exclude And Show In Advanced"),
(4, "Exclude"),
],
default=0,
help_text="""This sets the exclusion status of this entry.
</br>- (0) Include: shows entry & takes rank and prize.
</br>- (1) Exclude Prize and Show: shows entry, takes rank, but excludes from prizes
</br>- (2) Exclude and Show: shows entry, but excludes from rank and prizes
</br>- (3) Exclude and Show in Advanced: only shows entry in advanced views, excludes from rank and prizes
</br>- (4) Exclude: excludes entry from showing, rank, and prizes""",
),
),
migrations.AddField(
model_name="medalexclusionrecord",
name="exclusion_status",
field=models.IntegerField(
choices=[
(0, "Include"),
(1, "Exclude Prize And Show"),
(2, "Exclude And Show"),
(3, "Exclude And Show In Advanced"),
(4, "Exclude"),
],
default=4,
help_text="""This sets the exclusion status of this entry.
</br>- (0) Include: shows entry & takes rank and prize.
</br>- (1) Exclude Prize and Show: shows entry, takes rank, but excludes from prizes
</br>- (2) Exclude and Show: shows entry, but excludes from rank and prizes
</br>- (3) Exclude and Show in Advanced: only shows entry in advanced views, excludes from rank and prizes
</br>- (4) Exclude: excludes entry from showing, rank, and prizes""",
),
),
migrations.RunPython(migrate, reverse_code=reverse_migrate),
]
Loading
Loading