Skip to content

Commit 40a0ffa

Browse files
Backend: Add show_scores_on_leaderboard field to ChallengePhaseSplit model and update related components (#4985)
* Backend: Add show_scores_on_leaderboard field to ChallengePhaseSplit model and update related components * Frontend: Add unit test to ensure chosenMetrics is set to empty when show_scores_on_leaderboard is false
1 parent f24660a commit 40a0ffa

File tree

12 files changed

+165
-24
lines changed

12 files changed

+165
-24
lines changed

apps/challenges/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ class ChallengePhaseSplitAdmin(ImportExportTimeStampedAdmin):
264264
"leaderboard_decimal_precision",
265265
"is_leaderboard_order_descending",
266266
"show_execution_time",
267+
"show_scores_on_leaderboard",
267268
)
268269
list_filter = ("visibility",)
269270
search_fields = (

apps/challenges/migrations/0113_add_github_branch_field_and_unique_constraint.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Generated by Django 2.2.20 on 2025-07-09 15:00
22

3-
from django.db import migrations
3+
from django.db import migrations, models
44

55

66
class Migration(migrations.Migration):
@@ -10,23 +10,37 @@ class Migration(migrations.Migration):
1010
]
1111

1212
operations = [
13-
migrations.RunSQL(
14-
sql=(
15-
"DO $$\n"
16-
"BEGIN\n"
17-
" IF NOT EXISTS (\n"
18-
" SELECT 1 FROM information_schema.columns\n"
19-
" WHERE table_name='challenge'\n"
20-
" AND column_name='github_branch'\n"
21-
" ) THEN\n"
22-
" ALTER TABLE challenge ADD COLUMN github_branch "
23-
"varchar(200) NULL DEFAULT '';\n"
24-
" END IF;\n"
25-
"END$$;"
26-
),
27-
reverse_sql=(
28-
"ALTER TABLE challenge DROP COLUMN IF EXISTS github_branch;"
29-
),
13+
migrations.SeparateDatabaseAndState(
14+
state_operations=[
15+
migrations.AddField(
16+
model_name="challenge",
17+
name="github_branch",
18+
field=models.CharField(
19+
blank=True, default="", max_length=200, null=True
20+
),
21+
),
22+
],
23+
database_operations=[
24+
migrations.RunSQL(
25+
sql=(
26+
"DO $$\n"
27+
"BEGIN\n"
28+
" IF NOT EXISTS (\n"
29+
" SELECT 1 FROM information_schema.columns\n"
30+
" WHERE table_name='challenge'\n"
31+
" AND column_name='github_branch'\n"
32+
" ) THEN\n"
33+
" ALTER TABLE challenge ADD COLUMN github_branch "
34+
"varchar(200) NULL DEFAULT '';\n"
35+
" END IF;\n"
36+
"END$$;"
37+
),
38+
reverse_sql=(
39+
"ALTER TABLE challenge DROP COLUMN IF EXISTS "
40+
"github_branch;"
41+
),
42+
),
43+
],
3044
),
3145
# Add a partial unique constraint
3246
# Only applies when both fields are not empty
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.20 on 2026-02-05 17:56
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("challenges", "0114_add_require_complete_profile"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="challengephasesplit",
15+
name="show_scores_on_leaderboard",
16+
field=models.BooleanField(default=True),
17+
),
18+
]

apps/challenges/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ class ChallengePhaseSplit(TimeStampedModel):
502502
is_leaderboard_order_descending = models.BooleanField(default=True)
503503
show_leaderboard_by_latest_submission = models.BooleanField(default=False)
504504
show_execution_time = models.BooleanField(default=False)
505+
show_scores_on_leaderboard = models.BooleanField(default=True)
505506
# Allow ordering leaderboard by all metrics
506507
is_multi_metric_leaderboard = models.BooleanField(default=True)
507508

apps/challenges/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ class Meta:
190190
"visibility",
191191
"show_leaderboard_by_latest_submission",
192192
"show_execution_time",
193+
"show_scores_on_leaderboard",
193194
"leaderboard_schema",
194195
"is_multi_metric_leaderboard",
195196
)
@@ -384,6 +385,7 @@ class Meta:
384385
"is_leaderboard_order_descending",
385386
"show_leaderboard_by_latest_submission",
386387
"show_execution_time",
388+
"show_scores_on_leaderboard",
387389
)
388390

389391

apps/jobs/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ def calculate_distinct_sorted_leaderboard_data(
514514
team_list.append(data["submission__participant_team__team_name"])
515515

516516
leaderboard_labels = challenge_phase_split.leaderboard.schema["labels"]
517+
show_scores = challenge_phase_split.show_scores_on_leaderboard
517518
for item in distinct_sorted_leaderboard_data:
518519
item_result = []
519520
for index in leaderboard_labels:
@@ -529,6 +530,12 @@ def calculate_distinct_sorted_leaderboard_data(
529530
item["error"]["error_{0}".format(index)]
530531
for index in leaderboard_labels
531532
]
533+
534+
if not show_scores:
535+
item["result"] = []
536+
item["error"] = None
537+
item.pop("filtering_score", None)
538+
item.pop("filtering_error", None)
532539
return distinct_sorted_leaderboard_data, status.HTTP_200_OK
533540

534541

frontend/src/js/controllers/challengeCtrl.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,9 @@
10711071
vm.selectedPhaseSplit = response.data;
10721072
vm.sortLeaderboardTextOption = (vm.selectedPhaseSplit.show_leaderboard_by_latest_submission) ?
10731073
"Sort by best":"Sort by latest";
1074+
if (vm.selectedPhaseSplit.show_scores_on_leaderboard === false) {
1075+
vm.chosenMetrics = [];
1076+
}
10741077
},
10751078
onError: function (response) {
10761079
var error = response.data;
@@ -1105,7 +1108,11 @@
11051108

11061109
var leaderboardLabels = vm.leaderboard[i].leaderboard__schema.labels;
11071110
var index = leaderboardLabels.findIndex(label => label === vm.orderLeaderboardBy);
1108-
vm.chosenMetrics = index !== -1 ? [index.toString()]: undefined;
1111+
if (vm.selectedPhaseSplit && vm.selectedPhaseSplit.show_scores_on_leaderboard === false) {
1112+
vm.chosenMetrics = [];
1113+
} else {
1114+
vm.chosenMetrics = index !== -1 ? [index.toString()]: undefined;
1115+
}
11091116
vm.leaderboard[i]['submission__submitted_at_formatted'] = vm.leaderboard[i]['submission__submitted_at'];
11101117
vm.initial_ranking[vm.leaderboard[i].id] = i+1;
11111118
var dateTimeNow = moment(new Date());
@@ -3108,7 +3115,9 @@
31083115
};
31093116

31103117
vm.openLeaderboardDropdown = function() {
3111-
if (vm.chosenMetrics == undefined) {
3118+
if (vm.chosenMetrics == undefined &&
3119+
(!vm.selectedPhaseSplit || vm.selectedPhaseSplit.show_scores_on_leaderboard !== false) &&
3120+
vm.leaderboard && vm.leaderboard[0]) {
31123121
var index = [];
31133122
for (var k = 0; k < vm.leaderboard[0].leaderboard__schema.labels.length; k++) {
31143123
var label = vm.leaderboard[0].leaderboard__schema.labels[k].toString().trim();

frontend/src/views/web/challenge/leaderboard.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ <h5 class="w-300">Leaderboard</h5>
4040
</span>
4141
</div>
4242
</div>
43-
<div class="row" ng-if="challenge.isMultiMetricLeaderboardEnabled[challenge.phaseSplitId]">
43+
<div class="row" ng-if="challenge.isMultiMetricLeaderboardEnabled[challenge.phaseSplitId] && (!challenge.selectedPhaseSplit || challenge.selectedPhaseSplit.show_scores_on_leaderboard !== false)">
4444
<div class="col xs12 s6">
4545
<span>
4646
<md-select ng-model="challenge.orderLeaderboardBy" placeholder="Order by metric" class="rm-margin">
@@ -74,7 +74,7 @@ <h5 class="w-300">Leaderboard</h5>
7474
<label for="getAllEntries"></label>
7575
<span class="fs-16 w-300 complete-leaderboard"> {{ challenge.getAllEntriesTestOption }}</span>
7676
</div>
77-
<md-select name="leaderboarddropDownmenu" class="right" ng-model="challenge.chosenMetrics" placeholder="Visible Metrics" class="rm-margin" md-on-open="challenge.openLeaderboardDropdown()" md-on-close="challenge.openLeaderboardDropdown()" multiple>
77+
<md-select name="leaderboarddropDownmenu" class="right" ng-model="challenge.chosenMetrics" placeholder="Visible Metrics" class="rm-margin" md-on-open="challenge.openLeaderboardDropdown()" md-on-close="challenge.openLeaderboardDropdown()" multiple ng-if="!challenge.selectedPhaseSplit || challenge.selectedPhaseSplit.show_scores_on_leaderboard !== false">
7878
<md-option value="{{i}}" ng-repeat="(i, key) in challenge.leaderboard[0].leaderboard__schema.labels">
7979
<span ng-if="!challenge.leaderboardDropdown" class="fs-16 w-300">Visible Metrics</span>
8080
<span ng-if="challenge.leaderboardDropdown" class="fs-16 w-300">{{key}}</span>
@@ -107,7 +107,7 @@ <h5 class="w-300">Leaderboard</h5>
107107
</span>
108108
</a>
109109
</td>
110-
<td ng-repeat="(i, key) in challenge.leaderboard[0].leaderboard__schema.labels track by $index" ng-if="challenge.chosenMetrics.indexOf($index.toString()) !== -1">
110+
<td ng-repeat="(i, key) in challenge.leaderboard[0].leaderboard__schema.labels track by $index" ng-if="(challenge.chosenMetrics || []).indexOf($index.toString()) !== -1">
111111
<a href="#"
112112
ng-click="$parent.challenge.sortLeaderboard($parent.challenge, 'number', $index);">
113113
<span class="w-300 fs-20 leaderboard-label" ng-if="challenge.isMetricOrderedAscending(key) && key != challenge.orderLeaderboardBy ">{{key}} (&#x2193;) <span class="description" ng-if="challenge.getLabelDescription(key).length != 0">{{challenge.getLabelDescription(key)}}</span></span>
@@ -168,7 +168,7 @@ <h5 class="w-300">Leaderboard</h5>
168168
id="verified-badge" class="new badge green-background baseline-tag" data-badge-caption="V"
169169
ng-if="key.submission__is_verified_by_host"></span>
170170
</td>
171-
<td ng-repeat="(i, score) in key.result track by $index" ng-if="challenge.chosenMetrics.indexOf($index.toString()) !== -1">
171+
<td ng-repeat="(i, score) in key.result track by $index" ng-if="(challenge.chosenMetrics || []).indexOf($index.toString()) !== -1">
172172
<span class="w-400 fs-16" ng-if="challenge.leaderboard[0].leaderboard__schema.labels[i] == challenge.orderLeaderboardBy">{{score | number : challenge.selectedPhaseSplit.leaderboard_decimal_precision}}</span>
173173
<span class="w-300 fs-16" ng-if="challenge.leaderboard[0].leaderboard__schema.labels[i] != challenge.orderLeaderboardBy">{{score | number : challenge.selectedPhaseSplit.leaderboard_decimal_precision}}</span>
174174
<span class="new badge orange-background fs-16 w-300 partial-evaluation"

frontend/tests/controllers-test/challengeCtrl.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3085,6 +3085,32 @@ describe('Unit tests for challenge controller', function () {
30853085
expect(vm.showSubmissionMetaAttributesOnLeaderboard).toBe(false);
30863086
expect(vm.leaderboard[0].chosenMetrics).toBeUndefined();
30873087
});
3088+
3089+
it('should set chosenMetrics to empty when show_scores_on_leaderboard is false', function () {
3090+
var fakeLeaderboard = [{
3091+
id: 1,
3092+
leaderboard__schema: { labels: ['accuracy', 'loss'] },
3093+
orderLeaderboardBy: 'accuracy',
3094+
submission__submission_metadata: null,
3095+
submission__submitted_at: new Date(Date.now() - 2 * 365 * 24 * 60 * 60 * 1000).toISOString()
3096+
}];
3097+
spyOn(utilities, 'sendRequest').and.callFake(function (params) {
3098+
if (params.url && params.url.includes('/leaderboard/')) {
3099+
params.callback.onSuccess({ data: { results: fakeLeaderboard } });
3100+
} else if (params.url && params.url.includes('/challenge_phase_split/')) {
3101+
params.callback.onSuccess({
3102+
data: {
3103+
show_scores_on_leaderboard: false,
3104+
show_leaderboard_by_latest_submission: false
3105+
}
3106+
});
3107+
}
3108+
});
3109+
3110+
vm.getLeaderboard(123);
3111+
3112+
expect(vm.chosenMetrics).toEqual([]);
3113+
});
30883114
});
30893115

30903116
describe('Unit tests for re-run submission logic', function () {
@@ -3869,6 +3895,14 @@ describe('Unit tests for challenge controller', function () {
38693895
vm.openLeaderboardDropdown();
38703896
expect(vm.leaderboardDropdown).toBe(false);
38713897
});
3898+
3899+
it('should not populate chosenMetrics when show_scores_on_leaderboard is false', function () {
3900+
vm.chosenMetrics = undefined;
3901+
vm.selectedPhaseSplit = { show_scores_on_leaderboard: false };
3902+
vm.openLeaderboardDropdown();
3903+
expect(vm.chosenMetrics).toBeUndefined();
3904+
expect(vm.leaderboardDropdown).toBe(true);
3905+
});
38723906
});
38733907

38743908
describe('Unit tests for getTrophySize function', function () {

tests/unit/challenges/test_models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,17 @@ def test__str__(self):
231231
string_to_compare, self.challenge_phase_split.__str__()
232232
)
233233

234+
def test_show_scores_on_leaderboard_default(self):
235+
"""Test that show_scores_on_leaderboard defaults to True."""
236+
self.assertTrue(self.challenge_phase_split.show_scores_on_leaderboard)
237+
238+
def test_show_scores_on_leaderboard_can_be_false(self):
239+
"""Test that show_scores_on_leaderboard can be set to False."""
240+
self.challenge_phase_split.show_scores_on_leaderboard = False
241+
self.challenge_phase_split.save()
242+
self.challenge_phase_split.refresh_from_db()
243+
self.assertFalse(self.challenge_phase_split.show_scores_on_leaderboard)
244+
234245

235246
class LeaderboardDataTestCase(BaseTestCase):
236247
def setUp(self):

0 commit comments

Comments
 (0)