From 1ccd99f5a84b7bd372dad8cabfb3040ece01ef9c Mon Sep 17 00:00:00 2001 From: Dan LaManna Date: Wed, 29 Oct 2025 15:51:56 -0400 Subject: [PATCH] Significantly improve leaderboard performance --- requirements.txt | 1 + setup.py | 1 + stade/core/templates/leaderboards.html | 14 +++---- stade/core/tests/test_leaderboard.py | 42 ++++++++++----------- stade/core/views.py | 52 +++++++++++++++++--------- stade/tracker/rest/email.py | 2 +- 6 files changed, 66 insertions(+), 46 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9ab53f89..822c5671 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,6 +51,7 @@ markdown-it-py==3.0.0 mdurl==0.1.2 numpy==2.3.1 oauthlib==3.2.2 +orjson==3.11.4 packaging==25.0 pandas==2.3.1 pillow==10.4.0 diff --git a/setup.py b/setup.py index fda06c3a..19e4a291 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ 'djangorestframework', 'drf-yasg', 'isic-challenge-scoring>=5.8', + 'orjson', 'requests', 'rules', # See https://github.com/axnsan12/drf-yasg/issues/874 diff --git a/stade/core/templates/leaderboards.html b/stade/core/templates/leaderboards.html index 4a50a5d1..3a589179 100644 --- a/stade/core/templates/leaderboards.html +++ b/stade/core/templates/leaderboards.html @@ -89,17 +89,17 @@

{{ challenge.name }} Leaderbo {{ forloop.counter }} -
{{ submission.approach.team.name }}
- {% if submission.approach.team.institution %} +
{{ submission.approach__team__name }}
+ {% if submission.approach__team__institution %}
- {{ submission.approach.team.institution }} + {{ submission.approach__team__institution }}
{% endif %} - {{ submission.approach.name }} + {{ submission.approach__name }} - {% if submission.approach.manuscript_url %} - + {% if submission.approach__manuscript_url %} + description {% else %} @@ -107,7 +107,7 @@

{{ challenge.name }} Leaderbo {% endif %} - {% if submission.approach.uses_external_data %} + {% if submission.approach__uses_external_data %}
public Yes diff --git a/stade/core/tests/test_leaderboard.py b/stade/core/tests/test_leaderboard.py index 1e5d51d7..036e8831 100644 --- a/stade/core/tests/test_leaderboard.py +++ b/stade/core/tests/test_leaderboard.py @@ -81,18 +81,18 @@ def test_leaderboard_by_approach(task_with_submissions, client): submissions = resp.context['submissions'] first, second, third, fourth = submissions[:4] - assert first.approach.team.name == 'team_0' - assert first.approach.name == 'approach_0' - assert first.overall_score == 0.95 - assert second.approach.team.name == 'team_0' - assert second.approach.name == 'approach_1' - assert second.overall_score == 0.80 - assert third.approach.team.name == 'team_1' - assert third.approach.name == 'approach_0' - assert third.overall_score == 0.78 - assert fourth.approach.team.name == 'team_3' - assert fourth.approach.name == 'approach_1' - assert fourth.overall_score == 0.60 + assert first['approach__team__name'] == 'team_0' + assert first['approach__name'] == 'approach_0' + assert first['overall_score'] == 0.95 + assert second['approach__team__name'] == 'team_0' + assert second['approach__name'] == 'approach_1' + assert second['overall_score'] == 0.80 + assert third['approach__team__name'] == 'team_1' + assert third['approach__name'] == 'approach_0' + assert third['overall_score'] == 0.78 + assert fourth['approach__team__name'] == 'team_3' + assert fourth['approach__name'] == 'approach_1' + assert fourth['overall_score'] == 0.60 @pytest.mark.django_db @@ -109,12 +109,12 @@ def test_leaderboard_by_team(task_with_submissions, client): submissions = resp.context['submissions'] first, second, third = submissions[:3] - assert first.approach.team.name == 'team_5' - assert first.approach.name == 'approach_0' - assert first.overall_score == 0.95 - assert second.approach.team.name == 'team_6' - assert second.approach.name == 'approach_0' - assert second.overall_score == 0.78 - assert third.approach.team.name == 'team_8' - assert third.approach.name == 'approach_1' - assert third.overall_score == 0.60 + assert first['approach__team__name'] == 'team_5' + assert first['approach__name'] == 'approach_0' + assert first['overall_score'] == 0.95 + assert second['approach__team__name'] == 'team_6' + assert second['approach__name'] == 'approach_0' + assert second['overall_score'] == 0.78 + assert third['approach__team__name'] == 'team_8' + assert third['approach__name'] == 'approach_1' + assert third['overall_score'] == 0.60 diff --git a/stade/core/views.py b/stade/core/views.py index d40c9538..d9528eb9 100644 --- a/stade/core/views.py +++ b/stade/core/views.py @@ -1,5 +1,4 @@ from datetime import timedelta -import json import logging from django.conf import settings @@ -13,8 +12,10 @@ from django.urls import reverse from django.utils import timezone from django.views.decorators.http import require_http_methods +import orjson import requests from rules.contrib.views import objectgetter, permission_required +from s3_file_field import S3FileField from stade.core.forms import ( AcceptInvitationForm, @@ -68,28 +69,45 @@ def leaderboard_page(request, challenge): submission_ids = list(submissions_by_approach(active_task.id)) submissions = list( - Submission.objects.defer(None) # avoid n+1 queries on the normally deferred score field - .select_related('approach', 'approach__team', 'creator') - .filter(id__in=submission_ids) - .order_by('-overall_score', 'created') - )[ - 0:200 - ] # leaderboards only show the top 200 results + Submission.objects.filter(id__in=submission_ids) + .values( + 'id', + 'overall_score', + 'score', + 'created', + 'approach__name', + 'approach__team__name', + 'approach__team__institution', + 'approach__uses_external_data', + 'approach__manuscript', + ) + .order_by('-overall_score', 'created')[:200] + ) + field = S3FileField() for submission in submissions: - if submission.score and isinstance(submission.score, dict): - submission.score_json = json.dumps(submission.score) + score = submission['score'] + if score: + if isinstance(score, str): + submission['score_json'] = score + elif isinstance(score, dict): + submission['score_json'] = orjson.dumps(score).decode('utf-8') + else: + submission['score_json'] = '{}' else: - submission.score_json = '{}' + submission['score_json'] = '{}' + + if submission['approach__manuscript']: + submission['approach__manuscript_url'] = field.storage.url( + submission['approach__manuscript'] + ) + else: + submission['approach__manuscript_url'] = None stats = { 'total_submissions': len(submissions), - 'unique_teams': len( - set(s.approach.team.name for s in submissions if s.approach and s.approach.team) - ), - 'used_external_data': sum( - 1 for s in submissions if s.approach and s.approach.uses_external_data - ), + 'unique_teams': len(set(s['approach__team__name'] for s in submissions)), + 'used_external_data': sum(1 for s in submissions if s['approach__uses_external_data']), } return render( diff --git a/stade/tracker/rest/email.py b/stade/tracker/rest/email.py index ed73f3ab..cf74f206 100644 --- a/stade/tracker/rest/email.py +++ b/stade/tracker/rest/email.py @@ -12,7 +12,7 @@ class ConflictException(APIException): status_code = status.HTTP_500_INTERNAL_SERVER_ERROR default_detail = 'A server error occurred.' - def __init__(self, detail, field, code): + def __init__(self, detail, field, code): # noqa: B042 if code is not None: self.status_code = code if detail is not None: