Skip to content
7 changes: 4 additions & 3 deletions api/leaderboard/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@


@app.task(bind=True)
def atcoder_user_update(self):

Check failure on line 361 in api/leaderboard/celery.py

View workflow job for this annotation

GitHub Actions / Spell checking

`atcoder` is not a recognized word. (unrecognized-spelling)
import re
from bs4 import BeautifulSoup
from leaderboard.models import AtcoderUser
Expand Down Expand Up @@ -433,14 +433,15 @@
df = build_unified_ranking()
if df.empty:
return
for _, row in df.iterrows():

Check failure on line 436 in api/leaderboard/celery.py

View workflow job for this annotation

GitHub Actions / Spell checking

`iterrows` is not a recognized word. (unrecognized-spelling)
username = row.get("username")
if not username:
user_id = row.get("user_id")
if not user_id:
continue
UnifiedScoreHistory.objects.update_or_create(
username=username,
user_id=user_id,
date=today,
defaults={
"username": row.get("username"),
"total_score": float(row.get("total_score", 0)),
"github_score": float(row.get("github_score", 0)),
"cf_score": float(row.get("cf_score", 0)),
Expand Down
18 changes: 18 additions & 0 deletions api/leaderboard/migrations/0019_codechefuser_calendar_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2026-03-21 09:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("leaderboard", "0002_alter_codeforcesuser_last_activity_and_more"),
]

operations = [
migrations.AddField(
model_name="codechefuser",
name="calendar_data",
field=models.TextField(blank=True, default="[]"),
),
]
76 changes: 76 additions & 0 deletions api/leaderboard/migrations/0020_organization_organizationmember.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion

class Migration(migrations.Migration):

dependencies = [
("leaderboard", "0019_codechefuser_calendar_data"),
]

operations = [
migrations.CreateModel(
name="Organization",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=128)),
("description", models.TextField(blank=True, null=True)),
("is_private", models.BooleanField(default=True)),
(
"join_code",
models.CharField(blank=True, max_length=10, null=True, unique=True),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"admin",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="administered_organizations",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="OrganizationMember",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("joined_at", models.DateTimeField(auto_now_add=True)),
(
"organization",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="memberships",
to="leaderboard.organization",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="organization_memberships",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("organization", "user")},
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Generated by Django 5.1.5 on 2026-03-24 08:58

import django.db.models.deletion
import leaderboard.models
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("leaderboard", "0020_organization_organizationmember"),
]

operations = [
migrations.AddField(
model_name="leetcodeuser",
name="calendar_data",
field=models.TextField(blank=True, default="{}"),
),
migrations.AddField(
model_name="usernames",
name="ac_uname",
field=models.CharField(default="", max_length=64),
),
migrations.AddField(
model_name="usernames",
name="bio",
field=models.TextField(blank=True, default="", max_length=500),
),
migrations.AddField(
model_name="usernames",
name="location",
field=models.CharField(blank=True, default="", max_length=128),
),
migrations.AddField(
model_name="usernames",
name="occupation",
field=models.CharField(blank=True, default="", max_length=128),
),
migrations.AddField(
model_name="usernames",
name="organization",
field=models.CharField(blank=True, default="", max_length=128),
),
migrations.AlterField(
model_name="codeforcesuser",
name="last_activity",
field=models.BigIntegerField(
default=leaderboard.models.get_default_cf_last_activity
),
),
migrations.CreateModel(
name="Achievement",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("slug", models.CharField(max_length=64)),
("tier", models.CharField(max_length=64)),
("unlocked_at", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("user", "slug", "tier")},
},
),
migrations.CreateModel(
name="PostVote",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"vote_type",
models.CharField(
choices=[("like", "Like"), ("dislike", "Dislike")], max_length=7
),
),
(
"post",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="votes",
to="leaderboard.discussionpost",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("user", "post")},
},
),
]
18 changes: 18 additions & 0 deletions api/leaderboard/migrations/0022_alter_organization_join_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2026-03-28 11:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("leaderboard", "0021_leetcodeuser_calendar_data_usernames_ac_uname_and_more"),
]

operations = [
migrations.AlterField(
model_name="organization",
name="join_code",
field=models.CharField(blank=True, max_length=10, null=True, unique=True),
),
]
13 changes: 13 additions & 0 deletions api/leaderboard/migrations/0023_merge_20260329_1109.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Generated by Django 5.1.5 on 2026-03-29 11:09

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("leaderboard", "0019_codechefuser_calendar_data"),
("leaderboard", "0022_alter_organization_join_code"),
]

operations = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 5.1.5 on 2026-03-29 11:10

import leaderboard.models
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("leaderboard", "0023_merge_20260329_1109"),
]

operations = [
migrations.AlterField(
model_name="codeforcesuser",
name="last_activity",
field=models.BigIntegerField(
default=leaderboard.models.get_current_timestamp
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 5.1.5 on 2026-03-29 12:30

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("leaderboard", "0024_alter_codeforcesuser_last_activity"),
]

operations = [
migrations.AlterUniqueTogether(
name="unifiedscorehistory",
unique_together=set(),
),
migrations.AddField(
model_name="unifiedscorehistory",
name="user_id",
field=models.IntegerField(db_index=True, default=0),
),
migrations.AlterUniqueTogether(
name="unifiedscorehistory",
unique_together={("user_id", "date")},
),
]
Comment on lines +12 to +26
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

Migration will fail on existing data: missing data migration step.

The migration sequence:

  1. Removes old (username, date) constraint
  2. Adds user_id field with default=0
  3. Applies new (user_id, date) constraint

If any existing UnifiedScoreHistory rows share the same date, step 3 will fail with a unique constraint violation because they all have user_id=0.

Insert a RunPython data migration between steps 2 and 3 to backfill user_id from the username field by looking up the corresponding UserNames.user_id.

🛠️ Suggested migration structure
from django.db import migrations, models

def backfill_user_id(apps, schema_editor):
    UnifiedScoreHistory = apps.get_model('leaderboard', 'UnifiedScoreHistory')
    UserNames = apps.get_model('leaderboard', 'UserNames')
    
    # Build username->user_id mapping
    user_map = {}
    for un in UserNames.objects.select_related('user').all():
        # Use same logic as build_unified_ranking to find username
        username = un.gh_uname or un.cf_uname or un.cc_uname or un.lt_uname
        if username and un.user_id:
            user_map[username] = un.user_id
    
    for record in UnifiedScoreHistory.objects.all():
        if record.username in user_map:
            record.user_id = user_map[record.username]
            record.save(update_fields=['user_id'])

class Migration(migrations.Migration):
    dependencies = [
        ("leaderboard", "0024_alter_codeforcesuser_last_activity"),
    ]

    operations = [
        migrations.AlterUniqueTogether(
            name="unifiedscorehistory",
            unique_together=set(),
        ),
        migrations.AddField(
            model_name="unifiedscorehistory",
            name="user_id",
            field=models.IntegerField(db_index=True, default=0),
        ),
        migrations.RunPython(backfill_user_id, migrations.RunPython.noop),
        migrations.AlterUniqueTogether(
            name="unifiedscorehistory",
            unique_together={("user_id", "date")},
        ),
    ]
🧰 Tools
🪛 Ruff (0.15.7)

[warning] 12-26: Mutable default value for class attribute

(RUF012)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/leaderboard/migrations/0025_alter_unifiedscorehistory_unique_together_and_more.py`
around lines 12 - 26, Migration will fail because existing UnifiedScoreHistory
rows get default user_id=0 and the new unique_together (user_id, date) will
conflict; add a RunPython data migration between the AddField for user_id and
the final AlterUniqueTogether to backfill user_id from usernames. Implement a
backfill function (e.g., backfill_user_id) that uses
apps.get_model('leaderboard', 'UnifiedScoreHistory') and
apps.get_model('leaderboard', 'UserNames'), build a mapping from username to
user_id using the same logic as build_unified_ranking (username = gh_uname or
cf_uname or cc_uname or lt_uname) and then iterate UnifiedScoreHistory objects
setting record.user_id from that map and saving with update_fields=['user_id'];
register it as migrations.RunPython(backfill_user_id, migrations.RunPython.noop)
placed after AddField and before the AlterUniqueTogether that enforces
{('user_id','date')}.

3 changes: 2 additions & 1 deletion api/leaderboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ class UnifiedScoreHistory(models.Model):
per-platform breakdown. Written by a Celery beat task every day.
Used by the Unified trend analysis heatmap and line chart.
"""
user_id = models.IntegerField(db_index=True, default=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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for existing UnifiedScoreHistory rows and potential conflicts
ast-grep --pattern 'class UnifiedScoreHistory($$$):
  $$$
'

# Look for any existing data migration that backfills user_id
rg -n 'UnifiedScoreHistory' --glob '**/migrations/*.py' -A 5

Repository: OpenLake/Leaderboard-Pro

Length of output: 2658


🏁 Script executed:

# Get the full migration file to see if UnifiedScoreHistory is being created or altered
cat -n api/leaderboard/migrations/0002_alter_codeforcesuser_last_activity_and_more.py | head -100

Repository: OpenLake/Leaderboard-Pro

Length of output: 1963


🏁 Script executed:

# Check if there's a data migration that backfills user_id
rg -l 'RunPython|RunSQL' api/leaderboard/migrations/ | xargs -I {} grep -l 'user_id\|UnifiedScoreHistory' {} 2>/dev/null

Repository: OpenLake/Leaderboard-Pro

Length of output: 50


🏁 Script executed:

# Check trends_views.py for how it queries UnifiedScoreHistory
rg -n 'UnifiedScoreHistory' api/ -A 3 -B 1 | grep -E '(trends_views|\.filter|\.objects)'

Repository: OpenLake/Leaderboard-Pro

Length of output: 1872


🏁 Script executed:

# List all migrations in order
ls -la api/leaderboard/migrations/*.py | head -20

Repository: OpenLake/Leaderboard-Pro

Length of output: 1222


🏁 Script executed:

# Find migrations that modify UnifiedScoreHistory after 0002
rg -l 'UnifiedScoreHistory' api/leaderboard/migrations/*.py | sort

Repository: OpenLake/Leaderboard-Pro

Length of output: 146


🏁 Script executed:

# Check the celery task that updates UnifiedScoreHistory
rg -n 'update_or_create' api/leaderboard/celery.py -B 5 -A 10 | grep -A 10 'UnifiedScoreHistory'

Repository: OpenLake/Leaderboard-Pro

Length of output: 658


🏁 Script executed:

# Read migration 0025 that alters UnifiedScoreHistory
cat -n api/leaderboard/migrations/0025_alter_unifiedscorehistory_unique_together_and_more.py

Repository: OpenLake/Leaderboard-Pro

Length of output: 949


🏁 Script executed:

# Check if there are any migrations between 0002 and 0025 that might touch UnifiedScoreHistory
rg -n 'UnifiedScoreHistory' api/leaderboard/migrations/000*.py

Repository: OpenLake/Leaderboard-Pro

Length of output: 189


🏁 Script executed:

# Search for any data migrations (numbered migrations with RunPython) that might backfill user_id
find api/leaderboard/migrations -name "*.py" -type f -exec grep -l "RunPython\|raw_sql" {} \; | xargs grep -l "user_id\|UnifiedScoreHistory" 2>/dev/null

Repository: OpenLake/Leaderboard-Pro

Length of output: 52


🏁 Script executed:

# Check if there are any migrations after 0025 that might fix the user_id values
ls api/leaderboard/migrations/002*.py

Repository: OpenLake/Leaderboard-Pro

Length of output: 499


🏁 Script executed:

# Look at the celery task more broadly to understand the context
rg -n 'def.*unified' api/leaderboard/celery.py -i -A 20 | head -50

Repository: OpenLake/Leaderboard-Pro

Length of output: 912


Migration 0025 adds user_id field with default=0 but provides no data migration to backfill values from existing records.

Migration 0002 created UnifiedScoreHistory with unique_together=("username", "date"). Migration 0025 changes this to unique_together=("user_id", "date") and adds user_id=IntegerField(default=0).

Any existing records in the table will have user_id=0 after this migration. If multiple records share the same date (which is possible under the old unique constraint), the new constraint will fail because they'll all have (user_id=0, date=<same>).

A data migration must backfill user_id before the new constraint is applied, or the migration will cause integrity violations.

Additionally, trends_views.py filters by username only (lines 348, 396), but the new constraint is on user_id. Ensure the query logic aligns with this schema change to avoid returning incorrect results if data consistency issues exist.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/leaderboard/models.py` at line 278, UnifiedScoreHistory migration 0025
adds user_id=IntegerField(default=0) and changes unique_together to
("user_id","date") but doesn't backfill existing rows, which will cause
integrity errors; modify migration 0025 to include a data migration that looks
up the correct user.pk for each existing UnifiedScoreHistory.username (using the
User model or your canonical user lookup) and sets user_id accordingly before
altering unique_together, then remove the default=0 or set null=False only after
backfill; also update trends_views.py queries that currently filter by username
(the filters around the username usage) to filter by user_id (or to join/match
both username and user_id) so query logic aligns with the schema change and
avoids returning incorrect results.

username = models.CharField(max_length=64, db_index=True)
date = models.DateField(db_index=True)

Expand All @@ -291,7 +292,7 @@ class UnifiedScoreHistory(models.Model):

class Meta:
# one record per user per day
unique_together = ("username", "date")
unique_together = ("user_id", "date")
ordering = ["date"]
Comment on lines 293 to 296
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how trends_views.py queries UnifiedScoreHistory
rg -n 'UnifiedScoreHistory' --type py -B 2 -A 10 | head -100

Repository: OpenLake/Leaderboard-Pro

Length of output: 7785


🏁 Script executed:

# See the complete query and score_map logic in trends_views.py around the UnifiedScoreHistory filter
sed -n '330,420p' api/leaderboard/trends_views.py

Repository: OpenLake/Leaderboard-Pro

Length of output: 3617


🏁 Script executed:

# Check if username can be duplicate for different user_ids in the unified ranking
grep -n 'username' api/leaderboard/analytics.py | head -30

Repository: OpenLake/Leaderboard-Pro

Length of output: 2036


🏁 Script executed:

# Search for score_map usage in trends_views.py
rg -n 'score_map' api/leaderboard/trends_views.py -B 2 -A 2

Repository: OpenLake/Leaderboard-Pro

Length of output: 514


🏁 Script executed:

# Check the exact iteration logic in build_unified_ranking to confirm one row per User
sed -n '160,195p' api/leaderboard/analytics.py

Repository: OpenLake/Leaderboard-Pro

Length of output: 1712


🏁 Script executed:

# Check if username field constraint exists or if it's truly only unique on (user_id, date)
grep -A 5 'unique_together' api/leaderboard/models.py | grep -A 5 'UnifiedScoreHistory'

Repository: OpenLake/Leaderboard-Pro

Length of output: 210


🏁 Script executed:

# Verify the User model to understand if user.username is unique
rg -A 10 'class User' api/leaderboard/models.py

Repository: OpenLake/Leaderboard-Pro

Length of output: 1795


Data loss risk: score_map overwrites scores when multiple users share the same username.

In unified_heatmap() and unified_linechart() (trends_views.py lines 348–352, 396–408), queries filter UnifiedScoreHistory by username only:

records = UnifiedScoreHistory.objects.filter(username=username, date__gte=cutoff)
score_map = {r.date.isoformat(): r.total_score for r in records}

The constraint unique_together = ("user_id", "date") allows multiple records with the same username on the same date (if two users have identical usernames). When building the dict with date as the sole key, duplicate dates cause the last record to overwrite earlier ones, losing score data.

This occurs because username in UnifiedScoreHistory is derived as: platform handle (gh_uname, cf_uname, cc_uname, lt_uname) OR fallback to user.username. While user.username is globally unique, platform handles are not, enabling collisions across users.

Add db_index=True to the username field and either:

  1. Add unique_together = ("username", "date") to prevent simultaneous records, or
  2. Update views to filter by user_id instead of username (recommended).
🧰 Tools
🪛 Ruff (0.15.7)

[warning] 296-296: Mutable default value for class attribute

(RUF012)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/leaderboard/models.py` around lines 293 - 296, UnifiedScoreHistory
records can collide when different users share the same platform handle because
unified_heatmap() and unified_linechart() build score_map = {r.date.isoformat():
r.total_score} while querying by username, allowing multiple records with the
same date to overwrite each other; fix by changing the views unified_heatmap()
and unified_linechart() to query/filter by user_id (e.g.,
UnifiedScoreHistory.objects.filter(user_id=..., date__gte=cutoff)) instead of
username so each user’s records are isolated, and in the model add db_index=True
to the username field and either add unique_together = ("username", "date") in
UnifiedScoreHistory.Meta or keep just the user_id-based queries (recommended) to
prevent future collisions.


def __str__(self):
Expand Down
3 changes: 2 additions & 1 deletion api/leaderboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
MAX_DATE_TIMESTAMP = datetime.now().timestamp()

import requests
from django.db import connection
from django.db import connection, transaction
from django.db.utils import OperationalError
from rest_framework import generics, mixins, status
from rest_framework.response import Response
Expand Down Expand Up @@ -937,6 +937,7 @@ def get_queryset(self):
# User sees organizations they are members of
return Organization.objects.filter(memberships__user=self.request.user)

@transaction.atomic
def perform_create(self, serializer):
# Admin is the current user
organization = serializer.save(admin=self.request.user)
Expand Down
8 changes: 4 additions & 4 deletions app/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ html, body {
}

.App {
overflow-x: hidden;
max-width: 100%;
height: 100vh;
overflow-y: auto;
display: flex;
flex-direction: column;
min-height: 100vh;
width: 100%;
}

@media (prefers-reduced-motion: no-preference) {
Expand Down
Loading
Loading