Skip to content

Commit 901c7c0

Browse files
authored
add CP to global leaderboards (#224)
add CP (recency_weighted) to global leaderboards in a similar method to "excluded" entries
1 parent 8a81357 commit 901c7c0

File tree

11 files changed

+93
-30
lines changed

11 files changed

+93
-30
lines changed

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,13 @@ const LeaderboardTable: FC<Props> = ({
8282
{!!entriesToDisplay.length ? (
8383
entriesToDisplay.map((entry) => (
8484
<LeaderboardRow
85-
key={`ranking-row-${category}-${entry.user.id}`}
85+
key={`ranking-row-${category}-${entry.user ? entry.user.id : entry.aggregation_method!}`}
8686
rowEntry={entry}
87-
href={`/accounts/profile/${entry.user.id}?mode=medals`}
87+
href={
88+
entry.user
89+
? `/accounts/profile/${entry.user.id}?mode=medals`
90+
: `/questions/track-record`
91+
}
8892
/>
8993
))
9094
) : (

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ type Props = {
2121
};
2222

2323
const LeaderboardRow: FC<Props> = ({ rowEntry, href, isUserRow = false }) => {
24-
const { user, rank, contribution_count, score, medal } = rowEntry;
24+
const { user, aggregation_method, rank, contribution_count, score, medal } =
25+
rowEntry;
2526

2627
return (
2728
<tr
@@ -33,7 +34,7 @@ const LeaderboardRow: FC<Props> = ({ rowEntry, href, isUserRow = false }) => {
3334
},
3435
{
3536
"bg-purple-200 hover:bg-purple-300 dark:bg-purple-200-dark hover:dark:bg-purple-300-dark":
36-
!isUserRow && !!user.is_staff,
37+
!isUserRow && !!user?.is_staff,
3738
}
3839
)}
3940
>
@@ -56,7 +57,11 @@ const LeaderboardRow: FC<Props> = ({ rowEntry, href, isUserRow = false }) => {
5657
href={href}
5758
className="flex items-center truncate px-4 py-2.5 no-underline"
5859
>
59-
{user.username}
60+
{user
61+
? user.username
62+
: aggregation_method == "recency_weighted"
63+
? "Recency Weighted CP"
64+
: aggregation_method}
6065
</Link>
6166
</td>
6267
<td className="hidden w-24 p-0 font-mono text-base leading-4 @md:!table-cell">
@@ -102,7 +107,7 @@ export const UserLeaderboardRow: FC<UserLeaderboardRowProps> = ({
102107

103108
const userHref = userEntry.medal
104109
? "/medals"
105-
: `/contributions/?${SCORING_CATEGORY_FILTER}=${category}&${CONTRIBUTIONS_USER_FILTER}=${userEntry.user.id}&${SCORING_YEAR_FILTER}=${year}&${SCORING_DURATION_FILTER}=${duration}`;
110+
: `/contributions/?${SCORING_CATEGORY_FILTER}=${category}&${CONTRIBUTIONS_USER_FILTER}=${userEntry.user!.id}&${SCORING_YEAR_FILTER}=${year}&${SCORING_DURATION_FILTER}=${duration}`;
106111

107112
return <LeaderboardRow rowEntry={userEntry} href={userHref} isUserRow />;
108113
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const ProjectLeaderboardTable: FC<Props> = ({
7373
<tbody>
7474
{leaderboardEntries.map((entry) => (
7575
<TableRow
76-
key={entry.user.id}
76+
key={entry.user?.id ?? entry.aggregation_method}
7777
rowEntry={entry}
7878
userId={userId}
7979
withTake={withTake}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ const TableRow: FC<Props> = ({
2222
userId,
2323
prizePool,
2424
}) => {
25-
const { user, medal, rank, score, contribution_count, take, percent_prize } =
25+
const { user, aggregation_method, medal, rank, score, take, percent_prize } =
2626
rowEntry;
27-
const highlight = user.id === userId;
27+
const highlight = user?.id === userId;
2828

2929
return (
3030
<tr className="h-8">
@@ -37,7 +37,13 @@ const TableRow: FC<Props> = ({
3737
{rank}
3838
</Td>
3939
<Td className="sticky left-0 text-left" highlight={highlight}>
40-
<Link href={`/accounts/profile/${user.id}/`}>{user.username}</Link>
40+
<Link
41+
href={
42+
user ? `/accounts/profile/${user.id}/` : `questions/track-record/`
43+
}
44+
>
45+
{user ? user.username : aggregation_method}
46+
</Link>
4147
</Td>
4248
<Td className="text-right" highlight={highlight}>
4349
{score.toFixed(2)}

front_end/src/app/(main)/(leaderboards)/medals/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default async function Medals({
3535
MedalsPath.Profile;
3636

3737
const userMedals = await LeaderboardApi.getUserMedals(userId);
38-
const username = userMedals.at(0)?.user.username;
38+
const username = userMedals.at(0)?.user!.username;
3939

4040
return (
4141
<main className="mb-auto pb-3 text-blue-700 dark:text-blue-700-dark sm:px-3">

front_end/src/types/scoring.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export type MedalCategory = {
4141
};
4242

4343
export type LeaderboardEntry = {
44-
user: User;
44+
user: User | null;
45+
aggregation_method: string | null;
4546
score: number;
4647
rank: number | null;
4748
excluded: boolean;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 5.0.8 on 2024-08-21 21:50
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('scoring', '0009_score_aggregation_method_alter_score_user'),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name='leaderboardentry',
18+
name='aggregation_method',
19+
field=models.CharField(choices=[('recency_weighted', 'Recency Weighted'), ('unweighted', 'Unweighted'), ('single_aggregation', 'Single Aggregation')], max_length=200, null=True),
20+
),
21+
migrations.AlterField(
22+
model_name='leaderboardentry',
23+
name='user',
24+
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
25+
),
26+
]

scoring/models.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class ScoreTypes(models.TextChoices):
4747

4848

4949
class Leaderboard(TimeStampedModel):
50+
# typing
5051
id: int
5152
project_id: int
5253
objects: models.Manager["Leaderboard"]
@@ -138,9 +139,15 @@ def get_questions(self) -> list[Question]:
138139

139140

140141
class LeaderboardEntry(TimeStampedModel):
142+
# typing
143+
id: int
141144
objects: models.Manager["LeaderboardEntry"]
145+
user_id: int | None
142146

143-
user = models.ForeignKey(User, on_delete=models.CASCADE)
147+
user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
148+
aggregation_method = models.CharField(
149+
max_length=200, null=True, choices=AggregationMethod.choices
150+
)
144151
leaderboard = models.ForeignKey(
145152
Leaderboard, on_delete=models.CASCADE, related_name="entries", null=True
146153
)

scoring/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
class LeaderboardEntrySerializer(serializers.ModelSerializer):
88
user = BaseUserSerializer(required=False)
9+
aggregation_method = serializers.CharField()
910
score = serializers.FloatField()
1011
rank = serializers.IntegerField()
1112
excluded = serializers.BooleanField()
@@ -21,6 +22,7 @@ class Meta:
2122
model = LeaderboardEntry
2223
fields = [
2324
"user",
25+
"aggregation_method",
2426
"score",
2527
"rank",
2628
"excluded",

scoring/utils.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from posts.models import Post
1111
from projects.models import Project
1212
from questions.models import Question
13+
from questions.types import AggregationMethod
1314
from scoring.models import Score, LeaderboardEntry, Leaderboard, MedalExclusionRecord
1415
from scoring.score_math import evaluate_question
1516
from utils.the_math.formulas import string_location_to_bucket_index
@@ -58,34 +59,33 @@ def generate_scoring_leaderboard_entries(
5859
questions: list[Question],
5960
leaderboard: Leaderboard,
6061
) -> list[LeaderboardEntry]:
61-
# TODO: add aggregate scores
6262
scores: QuerySet[Score] = Score.objects.filter(
63-
user__isnull=False,
6463
question__in=questions,
6564
score_type=Leaderboard.ScoreTypes.get_base_score(leaderboard.score_type),
6665
)
67-
user_entries: dict[int, LeaderboardEntry] = {}
66+
entries: dict[int | AggregationMethod, LeaderboardEntry] = {}
6867
now = timezone.now()
6968
for score in scores:
70-
user_id = score.user_id
71-
if user_id not in user_entries:
72-
user_entries[user_id] = LeaderboardEntry(
73-
user_id=user_id,
69+
identifier = score.user_id or score.aggregation_method
70+
if identifier not in entries:
71+
entries[identifier] = LeaderboardEntry(
72+
user_id=score.user_id,
73+
aggregation_method=score.aggregation_method,
7474
score=0,
7575
coverage=0,
7676
contribution_count=0,
7777
calculated_on=now,
7878
)
79-
user_entries[user_id].score += score.score
80-
user_entries[user_id].coverage += score.coverage
81-
user_entries[user_id].contribution_count += 1
79+
entries[identifier].score += score.score
80+
entries[identifier].coverage += score.coverage
81+
entries[identifier].contribution_count += 1
8282
if leaderboard.score_type == Leaderboard.ScoreTypes.PEER_GLOBAL:
83-
for entry in user_entries.values():
83+
for entry in entries.values():
8484
entry.score /= max(30, entry.coverage)
8585
elif leaderboard.score_type == Leaderboard.ScoreTypes.PEER_GLOBAL_LEGACY:
86-
for entry in user_entries.values():
86+
for entry in entries.values():
8787
entry.score /= max(40, entry.contribution_count)
88-
return sorted(user_entries.values(), key=lambda entry: entry.score, reverse=True)
88+
return sorted(entries.values(), key=lambda entry: entry.score, reverse=True)
8989

9090

9191
def generate_comment_insight_leaderboard_entries(
@@ -221,20 +221,27 @@ def update_project_leaderboard(
221221
start_time__lte=leaderboard.finalize_time
222222
)
223223
excluded_users = exclusion_records.values_list("user", flat=True)
224+
excluded_user_ids = set([r.user.id for r in exclusion_records])
224225
# medals
225226
golds = silvers = bronzes = 0
226227
if (
227228
(leaderboard.project.type != "question_series")
228229
and leaderboard.finalize_time
229230
and (timezone.now() > leaderboard.finalize_time)
230231
):
231-
entry_count = len(new_entries)
232+
entry_count = len(
233+
[
234+
e
235+
for e in new_entries
236+
if (e.user_id and (e.user_id not in excluded_user_ids))
237+
]
238+
)
232239
golds = max(0.01 * entry_count, 1)
233240
silvers = max(0.01 * entry_count, 1)
234241
bronzes = max(0.03 * entry_count, 1)
235242
rank = 1
236243
for entry in new_entries:
237-
if entry.user.id in excluded_users:
244+
if (entry.user_id is None) or (entry.user_id in excluded_users):
238245
entry.excluded = True
239246
entry.medal = None
240247
entry.rank = rank
@@ -251,7 +258,9 @@ def update_project_leaderboard(
251258
for new_entry in new_entries:
252259
new_entry.leaderboard = leaderboard
253260
for previous_entry in previous_entries:
254-
if previous_entry.user == new_entry.user:
261+
if (previous_entry.user_id == new_entry.user_id) and (
262+
previous_entry.aggregation_method == new_entry.aggregation_method
263+
):
255264
new_entry.id = previous_entry.id
256265
seen.add(previous_entry)
257266
break

0 commit comments

Comments
 (0)