From 6b45aa9d96de976294134aed8245b82cb5765a09 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Fri, 25 Sep 2026 10:33:44 +0200 Subject: [PATCH 01/15] changed minio and mc location yet again --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4b2949dbb..b167880f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: # Minio local storage helper #---------------------------------------------------------------------------------------------------- minio: - image: quay.io/minio/minio:RELEASE.2025-04-22T22-12-26Z + image: codalab/minio:RELEASE.2025-04-22T22-12-26Z command: server /export volumes: - ./var/minio:/export @@ -69,7 +69,7 @@ services: interval: 5s retries: 5 createbuckets: - image: quay.io/minio/mc:RELEASE.2025-07-21T05-28-08Z + image: codalab/mc:RELEASE.2025-07-21T05-28-08Z depends_on: minio: condition: service_healthy From 54f2eb845b788d0ba25f349bca0654d1f797b5b6 Mon Sep 17 00:00:00 2001 From: didayolo Date: Fri, 25 Sep 2026 12:30:59 +0200 Subject: [PATCH 02/15] Fix fact sheets migration --- src/apps/competitions/models.py | 1 + src/apps/competitions/tests/test_phase_migration.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index a2934bf5b..af9d2f41c 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -166,6 +166,7 @@ def apply_phase_migration(self, current_phase, next_phase, force_migration=False owner=submission.owner, data=submission.data, organization=submission.organization, + fact_sheet_answers=submission.fact_sheet_answers, ) new_submission.save(ignore_submission_limit=True) new_submission.start() diff --git a/src/apps/competitions/tests/test_phase_migration.py b/src/apps/competitions/tests/test_phase_migration.py index f1b6e77a8..7e610c8fb 100644 --- a/src/apps/competitions/tests/test_phase_migration.py +++ b/src/apps/competitions/tests/test_phase_migration.py @@ -131,6 +131,14 @@ def test_only_parent_submissions_migrated(self): mock_sub_start = self.mock_migration() assert mock_sub_start.call_count == 1 + def test_fact_sheet_answers_are_migrated(self): + answers = {'method_name': 'my method', 'uses_external_data': 'false'} + self.phase1.submissions.update(fact_sheet_answers=answers) + self.mock_migration() + assert self.phase2.submissions.exists() + for submission in self.phase2.submissions.all(): + assert submission.fact_sheet_answers == answers + class PhaseStatusTests(TestCase): def setUp(self): From 91c6541922d132238d0e4b8f9dd83aeeed4ffa4c Mon Sep 17 00:00:00 2001 From: Idir Chikhoune Date: Mon, 21 Sep 2026 12:41:24 +0200 Subject: [PATCH 03/15] attempt to fix E2E multitask test --- .circleci/config.yml | 4 ++-- .env_circleci | 2 ++ src/settings/test.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ac57f8393..152fcf580 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -70,7 +70,7 @@ jobs: - run: name: "Tests: Run unit/integration tests (excluding e2e)" - command: docker compose exec django py.test src/ + command: docker compose exec -e CELERY_TASK_ALWAYS_EAGER=True django py.test src/ # We give the name of the test files manually because we need test_auth.py to be run before the others for state.json file to be created # CI="true" to skip some tests that fail in the CI for now @@ -78,7 +78,7 @@ jobs: name: "Tests: Run end-to-end (E2E) tests" command: | docker compose exec django python ./manage.py createsuperuser --no-input - cd tests && CI=True $HOME/.local/bin/uv run pytest test_auth.py test_account_creation.py test_competition.py test_submission.py + cd tests && $HOME/.local/bin/uv run pytest test_auth.py test_account_creation.py test_competition.py test_submission.py no_output_timeout: 30m # Example to run specific set of tests (for debugging individual tests from a batch of tests) diff --git a/.env_circleci b/.env_circleci index 9ec59cd51..79844788a 100644 --- a/.env_circleci +++ b/.env_circleci @@ -34,3 +34,5 @@ DJANGO_SUPERUSER_USERNAME=codabench DOMAIN_NAME=localhost:80 TLS_EMAIL=your@email.com SUBMISSIONS_API_URL=http://django:8000/api + +CELERY_TASK_ALWAYS_EAGER=False \ No newline at end of file diff --git a/src/settings/test.py b/src/settings/test.py index 4294dd940..a1ccf673f 100644 --- a/src/settings/test.py +++ b/src/settings/test.py @@ -1,9 +1,10 @@ +import os from settings.base import * # noqa: F401,F403 # these noqa comments are for flake8 ignores DEBUG = True -CELERY_TASK_ALWAYS_EAGER = True +CELERY_TASK_ALWAYS_EAGER = os.environ.get("CELERY_TASK_ALWAYS_EAGER", "True").lower() == "true" INSTALLED_APPS += ('debug_toolbar',) MIDDLEWARE = ('debug_toolbar.middleware.DebugToolbarMiddleware', 'querycount.middleware.QueryCountMiddleware', From 6dadb57a9a4ef64b9195b555753043210ce22a36 Mon Sep 17 00:00:00 2001 From: Idir Chikhoune Date: Mon, 21 Sep 2026 14:41:40 +0200 Subject: [PATCH 04/15] adapting test file to new conf --- tests/test_submission.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/tests/test_submission.py b/tests/test_submission.py index e23f1a6fb..2621883ac 100644 --- a/tests/test_submission.py +++ b/tests/test_submission.py @@ -2,14 +2,8 @@ import toml import pytest import re -import os from loguru import logger -if os.environ.get("CI", "false").lower() == "true": - ci = True -else: - ci = False - data = toml.load("config/config.toml") @@ -139,7 +133,6 @@ def test_v18_autowsl(page: Page): # Skip this test if in the CI -@pytest.mark.skipif(ci, reason="Works locally but fails in the CI because of CELERY_TASK_ALWAYS_EAGER = True") def test_v2_multiTask(page: Page) -> None: page.goto("/") page.get_by_role("link", name=" Benchmarks/Competitions").click() @@ -159,7 +152,7 @@ def test_v2_multiTask(page: Page) -> None: # Wait for Finished to show. If it does not, catch the error and reload the page in case the page didn't update automatically try: expect(page.get_by_role("cell", name="Finished")).to_be_visible(timeout=35000) - except: + except Exception: page.reload() expect(page.get_by_role("cell", name="Finished")).to_be_visible(timeout=2000) # Add to leaderboard and see if shows @@ -167,7 +160,7 @@ def test_v2_multiTask(page: Page) -> None: submission_Id = text.split(None, 1) try: page.locator("td:nth-child(6) > span > .icon").first.click(timeout=300) - except: + except Exception: page.locator("td:nth-child(7) > span > .icon").first.click(timeout=300) page.locator("div").filter(has_text=re.compile(r"^Results$")).click() expect( @@ -186,14 +179,13 @@ def test_v2_multiTask(page: Page) -> None: ).to_be_visible() found = True break - except: + except Exception: pass if not found: assert 0, "Submission not found in the leaderboard" # Skip this test if in the CI -@pytest.mark.skipif(ci, reason="Works locally but fails in the CI because of CELERY_TASK_ALWAYS_EAGER = True") def test_v2_multiTaskFactSheet(page: Page) -> None: page.goto("/") page.get_by_role("link", name=" Benchmarks/Competitions").click() From 772ab02e3de8cb7d4e2e9165432244b062f38a82 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 28 Sep 2026 21:21:05 +0500 Subject: [PATCH 05/15] Add security policy Add .github/SECURITY.md describing supported versions, how to report vulnerabilities privately (GitHub private vulnerability reporting or info@codabench.org), what to include in a report, and how reports are handled. --- .github/SECURITY.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 000000000..b24d71705 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,30 @@ +# Security Policy + +## Supported Versions + +Security fixes are applied to the `develop` branch and included in the next release. +Only the [latest release](https://github.com/codalab/codabench/releases/latest) is supported. +If you run your own Codabench instance, please keep it up to date. + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues, pull requests, or discussions.** + +Report them privately using one of the following channels: + +- **GitHub:** go to the [Security tab](https://github.com/codalab/codabench/security) of this repository and click **Report a vulnerability**. +- **Email:** send the details to [info@codabench.org](mailto:info@codabench.org) with `[SECURITY]` in the subject line. + +Please include as much of the following as possible: + +- Type of issue (e.g. XSS, SQL injection, privilege escalation, sandbox escape in the compute worker) +- Affected component (Django app, API endpoint, frontend page, compute worker, etc.) and file paths if known +- Affected version, commit, or URL +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code, if available +- Impact of the issue and how an attacker might exploit it + +## Handling of Reports + +- We will investigate, keep you informed of our progress, and let you know when a fix is released. +- Please give us reasonable time to fix the issue before disclosing it publicly. From 2f6b82cc64e92383d71e5f3810a88a8c35445f22 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 28 Sep 2026 11:39:32 +0500 Subject: [PATCH 06/15] Update external competitions list styling - Remove duplicate platform badge from the competition card corner (platform is still shown in the stats pill) - Increase intro text font size from 13px to 15px - Change page background to light blue - Make the filters panel height fit its content instead of stretching - Use a near-white background for the docs banner on this page - Change card hover to a lighter background with a blue border and shadow --- .../external_competition_list.tag | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/static/riot/external_competitions/external_competition_list.tag b/src/static/riot/external_competitions/external_competition_list.tag index 6b05bf892..7a88bd765 100644 --- a/src/static/riot/external_competitions/external_competition_list.tag +++ b/src/static/riot/external_competitions/external_competition_list.tag @@ -53,7 +53,6 @@
-
{competition.platform_name}
@@ -246,7 +245,7 @@ :scope display block margin-bottom 5px - background #f4f5f7 + background #e3ecf7 padding 20px border-radius 6px @@ -263,13 +262,16 @@ color #2c5a82 .external-blurb - font-size 13px + font-size 15px color #5c5c5c margin-bottom 20px max-width 900px // .external-competitions-banner / .external-btn live in // src/static/stylus/external_competitions.styl - shared with competitions/public-list.tag + // Only the background is overridden here, so the banner stays readable on the blue page + .external-competitions-banner + background #fafcff .content-container display flex @@ -278,6 +280,7 @@ .filters-panel width 250px flex-shrink 0 + align-self flex-start border 1px solid #ddd padding 10px margin-right 10px @@ -333,7 +336,6 @@ width 100% .tile-wrapper - position relative border solid 1px gainsboro display flex background-color #fff @@ -344,23 +346,10 @@ border-radius 5px .tile-wrapper:hover - box-shadow 0 3px 4px -1px #cac9c9ff + box-shadow 0 3px 8px -1px rgba(44, 90, 130, 0.3) transition all 75ms ease-in-out - background-color #e9f0f8 - border solid 1px #c8daee - - .platform-badge - position absolute - top 8px - right 8px - background #4684c7 - color #fff - font-size 11px - font-weight 600 - padding 3px 8px - border-radius 10px - text-transform uppercase - letter-spacing 0.03em + background-color #f7faff + border solid 1px #4684c7 .img-wrapper padding 5px From 16bcb79c034d086da06f2bf941ea21a076b5976a Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 29 Sep 2026 13:43:30 +0500 Subject: [PATCH 07/15] scroll to top on next or previous page load with a short delay --- .../external_competitions/external_competition_list.tag | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/static/riot/external_competitions/external_competition_list.tag b/src/static/riot/external_competitions/external_competition_list.tag index 7a88bd765..5c2e86f17 100644 --- a/src/static/riot/external_competitions/external_competition_list.tag +++ b/src/static/riot/external_competitions/external_competition_list.tag @@ -166,6 +166,12 @@ self.handle_ajax_pages = function (num) { self.update_competitions_list(self.get_url_page_number_or_default() + num) + .done(function () { + // Short pause so the user sees the new results appear before scrolling up + setTimeout(function () { + window.scrollTo({top: 0, behavior: 'smooth'}) + }, 200) + }) } self.update_competitions_list = function (num) { From 704d93cf1b4f96c9d17c73d5c6f74b3be071078f Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 29 Sep 2026 13:52:11 +0500 Subject: [PATCH 08/15] style updates for buttons to match the bluish theme, moved all css to the stylus file to keep at one place. --- .../external_competition_list.tag | 195 +---------------- src/static/stylus/external_competitions.styl | 199 ++++++++++++++++++ 2 files changed, 204 insertions(+), 190 deletions(-) diff --git a/src/static/riot/external_competitions/external_competition_list.tag b/src/static/riot/external_competitions/external_competition_list.tag index 5c2e86f17..86e575084 100644 --- a/src/static/riot/external_competitions/external_competition_list.tag +++ b/src/static/riot/external_competitions/external_competition_list.tag @@ -41,7 +41,7 @@
- +
@@ -80,11 +80,11 @@
- - + + { current_page } of {Math.ceil(competitions.count/competitions.page_size)} - - + +
@@ -243,189 +243,4 @@ self.update_competitions_list(self.get_url_page_number_or_default()) }) - - diff --git a/src/static/stylus/external_competitions.styl b/src/static/stylus/external_competitions.styl index f4aa537a3..3f65da0dd 100644 --- a/src/static/stylus/external_competitions.styl +++ b/src/static/stylus/external_competitions.styl @@ -28,3 +28,202 @@ background-color #396ca3 color #fff text-decoration none + +// External competitions list page (riot/external_competitions/external_competition_list.tag). +// Nested under the tag name so these generic class names don't leak to other pages. +external-competition-list + display block + width 100% + margin-bottom 5px + background #e3ecf7 + padding 20px + border-radius 6px + + .page-header + display flex + align-items center + justify-content space-between + margin-bottom 10px + + .page-title + margin 0 + font-size 24px + font-weight bold + color #2c5a82 + + .external-blurb + font-size 15px + color #5c5c5c + margin-bottom 20px + max-width 900px + + // Only the background is overridden here, so the banner stays readable on the blue page + .external-competitions-banner + background #fafcff + + .content-container + display flex + width 100% + + .filters-panel + width 250px + flex-shrink 0 + align-self flex-start + border 1px solid #ddd + padding 10px + margin-right 10px + margin-left 0 !important + background #fff + border-radius 5px + + input[type="text"] + width 100% + padding 5px + margin 5px 0 5px 0 + border 1px solid #ddd + border-radius 4px + + input[type="checkbox"] + margin-right 5px + + .filter-group + margin-bottom 20px + + .filter-group label + display block + font-size 13px + margin-bottom 6px + + .filter-label + font-size 14px + font-weight bold + display block + margin-bottom 8px + + .list-panel + flex-grow 1 + + .pagination-nav + padding 10px 0 + width 100% + text-align center + margin-bottom 20px + + // Blue buttons matching .external-btn; chained with .ui.button to beat Semantic UI's gray defaults + .ui.button.theme-btn + background-color #4684c7 + color #fff + transition background-color 0.2s ease + + &:hover, &:focus + background-color #396ca3 + color #fff + + // Semantic UI forces opacity .45 on disabled buttons; use explicit light blue colors instead + &.disabled, &:disabled + background-color #c8daee + color #7a9cc0 + opacity 1 !important + + .float-left + float left + + .float-right + float right + + .link-no-deco + all unset + text-decoration none + cursor pointer + width 100% + + .full-width + width 100% + + .tile-wrapper + border solid 1px gainsboro + display flex + background-color #fff + transition all 75ms ease-in-out + width 100% + margin-bottom 6px + padding 1em + border-radius 5px + + .tile-wrapper:hover + box-shadow 0 3px 8px -1px rgba(44, 90, 130, 0.3) + transition all 75ms ease-in-out + background-color #f7faff + border solid 1px #4684c7 + + .img-wrapper + padding 5px + align-self center + + img + max-height 60px !important + max-width 60px !important + margin 0 auto + + .comp-info + width 100% + + .comp-info .heading + text-align left + padding 5px + color #1b1b1b + margin-bottom 0.3em + + .comp-info .comp-description + text-align left + font-size 13px + line-height 1.15em + margin 0.35em + color #555 + + .comp-stats + display flex + flex-wrap wrap + gap 1em + font-size 0.9em + align-items center + margin-top 0.5em + padding 0 0.35em + color #555 + + .comp-stats > div + display flex + align-items center + gap 0.4em + background #eef4fb + border-radius 12px + padding 0.3em 0.7em + + .stat-label + color #888 + + .stat-value + color #333 + font-weight normal + + .loading-indicator + display flex + align-items center + padding 20px + width 100% + margin 0 auto + + .spinner + border 4px solid rgba(0,0,0,.1) + width 36px + height 36px + border-radius 50% + border-top-color #3498db + animation spin 1s ease-in-out infinite + +// Same definition as the spin keyframes in the other riot tags (keyframes are global either way) +@keyframes spin + 0% + transform rotate(0deg) + 100% + transform rotate(360deg) From fc271d79599a6e153d508926ba12febb04455b7e Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 28 Sep 2026 21:56:27 +0500 Subject: [PATCH 09/15] Hide queue connection details in competition details The competition details API now returns only the queue's id and name, so queue connection details (broker URL, credentials and vhost) and the queue's owner and organizers are no longer included. Add tests confirming this for logged-out users, competition creators who don't own the queue, and competition creators who own the queue. --- src/apps/api/serializers/competitions.py | 4 +- src/apps/api/serializers/queues.py | 8 +++ src/apps/api/tests/test_competitions.py | 62 +++++++++++++++++++++++- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/apps/api/serializers/competitions.py b/src/apps/api/serializers/competitions.py index 3fa4eb041..37a6412f8 100644 --- a/src/apps/api/serializers/competitions.py +++ b/src/apps/api/serializers/competitions.py @@ -15,7 +15,7 @@ from profiles.models import User from tasks.models import Task -from api.serializers.queues import QueueSerializer +from api.serializers.queues import QueueSerializer, QueuePublicSerializer from datetime import datetime from django.utils.timezone import now @@ -375,7 +375,7 @@ class CompetitionDetailSerializer(serializers.ModelSerializer): participant_status = serializers.CharField(read_only=True) participants_count = serializers.IntegerField(read_only=True) submissions_count = serializers.IntegerField(read_only=True) - queue = QueueSerializer(read_only=True) + queue = QueuePublicSerializer(read_only=True) whitelist_emails = serializers.SerializerMethodField() class Meta: diff --git a/src/apps/api/serializers/queues.py b/src/apps/api/serializers/queues.py index c1818896e..76a3dd722 100644 --- a/src/apps/api/serializers/queues.py +++ b/src/apps/api/serializers/queues.py @@ -64,6 +64,14 @@ def validate(self, attrs): return super().validate(attrs) +class QueuePublicSerializer(serializers.ModelSerializer): + """Minimal queue info safe to expose on public endpoints (no broker credentials).""" + class Meta: + model = Queue + fields = ('id', 'name') + read_only_fields = fields + + class QueueSerializer(QueueOwnerMixin, serializers.ModelSerializer): is_owner = serializers.SerializerMethodField() owner = serializers.CharField(source='owner.username', read_only=True) diff --git a/src/apps/api/tests/test_competitions.py b/src/apps/api/tests/test_competitions.py index a8e902b63..d4122a49e 100644 --- a/src/apps/api/tests/test_competitions.py +++ b/src/apps/api/tests/test_competitions.py @@ -1,6 +1,7 @@ import json import random import csv +import uuid from zipfile import ZipFile from io import StringIO, BytesIO from unittest import mock @@ -10,7 +11,7 @@ from api.serializers.competitions import CompetitionSerializer from competitions.models import CompetitionParticipant, Submission, Competition from factories import UserFactory, CompetitionFactory, CompetitionParticipantFactory, PhaseFactory, LeaderboardFactory, \ - ColumnFactory, SubmissionFactory, SubmissionScoreFactory, TaskFactory + ColumnFactory, SubmissionFactory, SubmissionScoreFactory, TaskFactory, QueueFactory class CompetitionTests(APITestCase): @@ -84,6 +85,65 @@ def test_delete_own_competition(self): assert not Competition.objects.filter(pk=self.comp.pk).exists() +class CompetitionDetailQueueTests(APITestCase): + def setUp(self): + self.creator = UserFactory(username='creator', password='creator') + self.queue_owner = UserFactory( + username='queue_owner', + password='queue_owner', + rabbitmq_username='queue-owner-rabbit-user', + rabbitmq_password='queue-owner-rabbit-password', + ) + # Mock RabbitMQ so saving the queue doesn't create a real vhost; return a fake vhost UUID instead + with mock.patch('queues.models.rabbit.create_queue') as rabbit_create_queue: + rabbit_create_queue.return_value = uuid.uuid4() + self.queue = QueueFactory(owner=self.queue_owner, is_public=True) + self.comp = CompetitionFactory(created_by=self.creator, queue=self.queue, published=True) + self.url = reverse('competition-detail', kwargs={"pk": self.comp.pk}) + self.queue_owner_comp = CompetitionFactory(created_by=self.queue_owner, queue=self.queue, published=True) + self.queue_owner_comp_url = reverse('competition-detail', kwargs={"pk": self.queue_owner_comp.pk}) + + def _assert_queue_has_no_sensitive_details(self, resp): + """ + Check that the response succeeded and its queue field contains only the queue id and name. + Expects the queue owner's RabbitMQ username/password and the queue vhost + to be absent from the whole response body. + """ + assert resp.status_code == 200 + assert resp.data['queue'] == {'id': self.queue.id, 'name': self.queue.name} + content = resp.content.decode() + assert self.queue_owner.rabbitmq_username not in content + assert self.queue_owner.rabbitmq_password not in content + assert str(self.queue.vhost) not in content + + def test_anonymous_user_does_not_see_queue_broker_details(self): + """ + A logged-out user requests the competition detail API. + Expects a 200 response where the queue has only id and name, with no broker credentials or vhost. + """ + resp = self.client.get(self.url) + self._assert_queue_has_no_sensitive_details(resp) + + def test_competition_creator_not_owning_queue_does_not_see_queue_broker_details(self): + """ + The competition creator (who does not own the queue) requests the competition detail API. + Expects a 200 response where the queue has only id and name, with no broker credentials or vhost. + """ + self.client.login(username='creator', password='creator') + resp = self.client.get(self.url) + self._assert_queue_has_no_sensitive_details(resp) + + def test_competition_creator_who_owns_queue_does_not_see_queue_broker_details(self): + """ + The queue owner requests the detail API of their own competition that uses their queue. + Expects a 200 response where the queue has only id and name, with no broker credentials or vhost. + Queue owners can get the broker URL from the queues API instead. + """ + self.client.login(username='queue_owner', password='queue_owner') + resp = self.client.get(self.queue_owner_comp_url) + self._assert_queue_has_no_sensitive_details(resp) + + class CompetitionListTests(APITestCase): def setUp(self): self.user = UserFactory(username='user', password='user') From d677277bdee07d45837b401df5f0935778576a54 Mon Sep 17 00:00:00 2001 From: Obada Haddad-Soussac <11889208+ObadaS@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:54:33 +0200 Subject: [PATCH 10/15] Update version.json --- version.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.json b/version.json index 272184d28..c155bea47 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "tag_name": "v1.32", - "release_name": "v1.32", - "html_url": "https://github.com/codalab/codabench/releases/tag/v1.32" + "tag_name": "v1.33", + "release_name": "v1.33", + "html_url": "https://github.com/codalab/codabench/releases/tag/v1.33" } From 71a371eeefd49ccefc14b07bb2a96b1447eb4fb5 Mon Sep 17 00:00:00 2001 From: Obada Haddad-Soussac <11889208+ObadaS@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:31:38 +0200 Subject: [PATCH 11/15] Merge pull request #2570 from codalab/better_account_delete_message Update account deletion pop up --- src/apps/pages/views.py | 2 -- src/static/riot/profiles/profile_account.tag | 8 ++++++-- src/templates/404.html | 2 +- src/templates/admin/base_site.html | 2 +- src/templates/profiles/user_account.html | 1 + src/utils/context_processors.py | 1 + 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/apps/pages/views.py b/src/apps/pages/views.py index c6bb6a65e..d68e5da4d 100644 --- a/src/apps/pages/views.py +++ b/src/apps/pages/views.py @@ -5,7 +5,6 @@ from competitions.models import Submission from announcements.models import Announcement, NewsPost -from django.conf import settings from utils.data import pretty_bytes @@ -20,7 +19,6 @@ def get_context_data(self, *args, **kwargs): news_posts = NewsPost.objects.all().order_by('-id') context['news_posts'] = news_posts - context['CONTACT_EMAIL'] = settings.CONTACT_EMAIL return context diff --git a/src/static/riot/profiles/profile_account.tag b/src/static/riot/profiles/profile_account.tag index b6e6c68d0..fc41a2adb 100644 --- a/src/static/riot/profiles/profile_account.tag +++ b/src/static/riot/profiles/profile_account.tag @@ -15,6 +15,10 @@ This is extremely important. +
+ + If you just want to change your username, contact us instead at { contact_email } +

By clicking "Delete my account" you will receive a confirmation email to proceed with your account deletion. @@ -25,7 +29,7 @@

You will also no longer be eligible for any cash prizes in competitions you are participating in.

- You will not be able to re-create an account using the same email address for 30 days. + You will not be able to re-create an account using the same email address for 30 days.

@@ -53,7 +57,7 @@ {% endblock %} diff --git a/src/utils/context_processors.py b/src/utils/context_processors.py index 065758095..8b432d95e 100644 --- a/src/utils/context_processors.py +++ b/src/utils/context_processors.py @@ -58,4 +58,5 @@ def common_settings(request): 'VERSION_INFO': version_info, 'HOME_PAGE_COUNTERS_INFO': home_page_counters_info, 'DOMAIN_NAME': settings.DOMAIN_NAME, + 'CONTACT_EMAIL': settings.CONTACT_EMAIL, } From c08299bdd166242c5f2e34dc1b5996066234ace6 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 28 Sep 2026 11:57:15 +0500 Subject: [PATCH 12/15] Order competitions newest first in CompetitionViewSet. With this change, now users will see latest competitions first in Organizing and Participating tabs. --- src/apps/api/views/competitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index 88be3c012..4a3bacb00 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -187,7 +187,7 @@ def get_queryset(self): if search_query: qs = qs.filter(Q(title__icontains=search_query) | Q(description__icontains=search_query)) - qs = qs.order_by('created_when') + qs = qs.order_by('-created_when') return qs def get_permissions(self): From f481a179c3986b02ea9126a1d6af542c5a16c096 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 29 Sep 2026 16:14:04 +0500 Subject: [PATCH 13/15] Some competition fields are now hidden from non-admins (participants or other regular users) --- src/apps/api/serializers/competitions.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/apps/api/serializers/competitions.py b/src/apps/api/serializers/competitions.py index 37a6412f8..ce5bcf81d 100644 --- a/src/apps/api/serializers/competitions.py +++ b/src/apps/api/serializers/competitions.py @@ -446,10 +446,24 @@ def to_representation(self, instance): representation = super().to_representation(instance) user = self.context['request'].user - # If user is not admin/creator/collaborator then do not include secret_key and whitelist_emails + # If user is not admin/creator/collaborator then do not include the following fields if not instance.user_has_admin_permission(user): representation.pop('secret_key', None) representation.pop('whitelist_emails', None) + representation.pop('collaborators', None) + representation.pop('queue', None) + representation.pop('enable_detailed_results', None) + representation.pop('show_detailed_results_in_submission_panel', None) + representation.pop('show_detailed_results_in_leaderboard', None) + representation.pop('auto_run_submissions', None) + representation.pop('forum', None) + representation.pop('forum_enabled', None) + representation.pop('enable_human_in_the_loop', None) + representation.pop('registration_auto_approve', None) + representation.pop('can_participants_make_submissions_public', None) + representation.pop('make_programs_available', None) + representation.pop('make_input_data_available', None) + representation.pop('fact_sheet', None) return representation From b4f9d17feb850ec2ebcb3b0ab024f8250ebc53f6 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 29 Sep 2026 16:23:50 +0500 Subject: [PATCH 14/15] code cleaned --- src/apps/api/serializers/competitions.py | 40 ++++++++++++++---------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/apps/api/serializers/competitions.py b/src/apps/api/serializers/competitions.py index ce5bcf81d..f2a4faf19 100644 --- a/src/apps/api/serializers/competitions.py +++ b/src/apps/api/serializers/competitions.py @@ -378,6 +378,26 @@ class CompetitionDetailSerializer(serializers.ModelSerializer): queue = QueuePublicSerializer(read_only=True) whitelist_emails = serializers.SerializerMethodField() + # Fields only visible to competition admins (creator, collaborators, staff/superusers) + ADMIN_ONLY_FIELDS = ( + 'secret_key', + 'whitelist_emails', + 'collaborators', + 'queue', + 'enable_detailed_results', + 'show_detailed_results_in_submission_panel', + 'show_detailed_results_in_leaderboard', + 'auto_run_submissions', + 'forum', + 'forum_enabled', + 'enable_human_in_the_loop', + 'registration_auto_approve', + 'can_participants_make_submissions_public', + 'make_programs_available', + 'make_input_data_available', + 'fact_sheet', + ) + class Meta: model = Competition fields = ( @@ -446,24 +466,10 @@ def to_representation(self, instance): representation = super().to_representation(instance) user = self.context['request'].user - # If user is not admin/creator/collaborator then do not include the following fields + # If user is not admin/creator/collaborator then do not include the admin-only fields if not instance.user_has_admin_permission(user): - representation.pop('secret_key', None) - representation.pop('whitelist_emails', None) - representation.pop('collaborators', None) - representation.pop('queue', None) - representation.pop('enable_detailed_results', None) - representation.pop('show_detailed_results_in_submission_panel', None) - representation.pop('show_detailed_results_in_leaderboard', None) - representation.pop('auto_run_submissions', None) - representation.pop('forum', None) - representation.pop('forum_enabled', None) - representation.pop('enable_human_in_the_loop', None) - representation.pop('registration_auto_approve', None) - representation.pop('can_participants_make_submissions_public', None) - representation.pop('make_programs_available', None) - representation.pop('make_input_data_available', None) - representation.pop('fact_sheet', None) + for field in self.ADMIN_ONLY_FIELDS: + representation.pop(field, None) return representation From f64c302c8639e594fd5629f582ccb20b337b9b8d Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 29 Sep 2026 16:49:38 +0500 Subject: [PATCH 15/15] Comp Detail API Tests added. QueueTests merged into CompetitionDetailTests --- src/apps/api/tests/test_competitions.py | 213 ++++++++++++++++++++---- 1 file changed, 176 insertions(+), 37 deletions(-) diff --git a/src/apps/api/tests/test_competitions.py b/src/apps/api/tests/test_competitions.py index d4122a49e..307aa93b9 100644 --- a/src/apps/api/tests/test_competitions.py +++ b/src/apps/api/tests/test_competitions.py @@ -8,7 +8,7 @@ from django.urls import reverse from rest_framework.test import APITestCase -from api.serializers.competitions import CompetitionSerializer +from api.serializers.competitions import CompetitionSerializer, CompetitionDetailSerializer from competitions.models import CompetitionParticipant, Submission, Competition from factories import UserFactory, CompetitionFactory, CompetitionParticipantFactory, PhaseFactory, LeaderboardFactory, \ ColumnFactory, SubmissionFactory, SubmissionScoreFactory, TaskFactory, QueueFactory @@ -85,63 +85,202 @@ def test_delete_own_competition(self): assert not Competition.objects.filter(pk=self.comp.pk).exists() -class CompetitionDetailQueueTests(APITestCase): +class CompetitionDetailTests(APITestCase): + """ + Tests for the competition detail API: who can access public and private competitions, + which fields admins and non-admins see, and that the queue only includes its id and name. + """ def setUp(self): self.creator = UserFactory(username='creator', password='creator') - self.queue_owner = UserFactory( - username='queue_owner', - password='queue_owner', - rabbitmq_username='queue-owner-rabbit-user', - rabbitmq_password='queue-owner-rabbit-password', - ) + self.collaborator = UserFactory(username='collaborator', password='collaborator') + self.participant = UserFactory(username='participant', password='participant') + self.pending_participant = UserFactory(username='pending_participant', password='pending_participant') + self.other_user = UserFactory(username='other_user', password='other_user') + self.superuser = UserFactory(username='superuser', password='superuser', is_superuser=True, is_staff=True) + self.queue_owner = UserFactory(username='queue_owner', password='queue_owner') # Mock RabbitMQ so saving the queue doesn't create a real vhost; return a fake vhost UUID instead with mock.patch('queues.models.rabbit.create_queue') as rabbit_create_queue: rabbit_create_queue.return_value = uuid.uuid4() self.queue = QueueFactory(owner=self.queue_owner, is_public=True) - self.comp = CompetitionFactory(created_by=self.creator, queue=self.queue, published=True) - self.url = reverse('competition-detail', kwargs={"pk": self.comp.pk}) + + self.public_comp = CompetitionFactory( + created_by=self.creator, collaborators=[self.collaborator], queue=self.queue, published=True + ) + self.private_comp = CompetitionFactory( + created_by=self.creator, collaborators=[self.collaborator], queue=self.queue, published=False + ) + for comp in (self.public_comp, self.private_comp): + CompetitionParticipantFactory(user=self.participant, competition=comp, status='approved') + CompetitionParticipantFactory(user=self.pending_participant, competition=comp, status='pending') self.queue_owner_comp = CompetitionFactory(created_by=self.queue_owner, queue=self.queue, published=True) - self.queue_owner_comp_url = reverse('competition-detail', kwargs={"pk": self.queue_owner_comp.pk}) - def _assert_queue_has_no_sensitive_details(self, resp): + # One visible and one hidden leaderboard on the public competition + self.visible_leaderboard = LeaderboardFactory(hidden=False) + self.hidden_leaderboard = LeaderboardFactory(hidden=True) + PhaseFactory(competition=self.public_comp, leaderboard=self.visible_leaderboard) + PhaseFactory(competition=self.public_comp, leaderboard=self.hidden_leaderboard) + + # None means a logged-out user + self.admins = [self.creator, self.collaborator, self.superuser] + self.non_admins = [None, self.participant, self.pending_participant, self.other_user] + + def _get(self, competition, user=None, **params): + """Request the detail API of `competition` as `user`, or logged out when `user` is None.""" + self.client.logout() + if user: + self.client.force_login(user) + url = reverse('competition-detail', kwargs={"pk": competition.pk}) + return self.client.get(url, params) + + # ---------- Access ---------- + + def test_anyone_can_access_public_competition(self): """ - Check that the response succeeded and its queue field contains only the queue id and name. - Expects the queue owner's RabbitMQ username/password and the queue vhost - to be absent from the whole response body. + Admins, participants, other users and logged-out users request a published competition. + Expects a 200 response for all of them. """ - assert resp.status_code == 200 - assert resp.data['queue'] == {'id': self.queue.id, 'name': self.queue.name} - content = resp.content.decode() - assert self.queue_owner.rabbitmq_username not in content - assert self.queue_owner.rabbitmq_password not in content - assert str(self.queue.vhost) not in content + for user in self.admins + self.non_admins: + assert self._get(self.public_comp, user).status_code == 200 - def test_anonymous_user_does_not_see_queue_broker_details(self): + def test_organizers_approved_participants_and_superusers_can_access_private_competition(self): """ - A logged-out user requests the competition detail API. - Expects a 200 response where the queue has only id and name, with no broker credentials or vhost. + The creator, a collaborator, an approved participant and a superuser request an unpublished competition. + Expects a 200 response for all of them. """ - resp = self.client.get(self.url) - self._assert_queue_has_no_sensitive_details(resp) + for user in [self.creator, self.collaborator, self.participant, self.superuser]: + assert self._get(self.private_comp, user).status_code == 200 - def test_competition_creator_not_owning_queue_does_not_see_queue_broker_details(self): + def test_other_users_cannot_access_private_competition(self): """ - The competition creator (who does not own the queue) requests the competition detail API. - Expects a 200 response where the queue has only id and name, with no broker credentials or vhost. + A logged-out user, an unrelated user and a pending participant request an unpublished competition + without a secret key. + Expects a 404 response for all of them. """ - self.client.login(username='creator', password='creator') - resp = self.client.get(self.url) - self._assert_queue_has_no_sensitive_details(resp) + for user in [None, self.other_user, self.pending_participant]: + assert self._get(self.private_comp, user).status_code == 404 + + def test_valid_secret_key_gives_access_to_private_competition(self): + """ + A logged-out user, an unrelated user and a pending participant request an unpublished competition + with its secret key. + Expects a 200 response for all of them. + """ + for user in [None, self.other_user, self.pending_participant]: + resp = self._get(self.private_comp, user, secret_key=str(self.private_comp.secret_key)) + assert resp.status_code == 200 + + def test_invalid_secret_key_does_not_give_access_to_private_competition(self): + """ + A logged-out user and an unrelated user request an unpublished competition with a wrong secret key. + Expects a 404 response for both. + """ + for user in [None, self.other_user]: + resp = self._get(self.private_comp, user, secret_key=str(uuid.uuid4())) + assert resp.status_code == 404 + + # ---------- Fields ---------- + + def test_admins_see_admin_only_fields(self): + """ + The creator, a collaborator and a superuser request a published competition. + Expects every admin-only field in the response, including the competition's secret key. + """ + for user in self.admins: + resp = self._get(self.public_comp, user) + assert resp.status_code == 200 + for field in CompetitionDetailSerializer.ADMIN_ONLY_FIELDS: + assert field in resp.data, field + assert resp.data['secret_key'] == str(self.public_comp.secret_key) + + def test_non_admins_do_not_see_admin_only_fields(self): + """ + Participants, an unrelated user and a logged-out user request a published competition. + Expects the public fields in the response, no admin-only field, + and the competition's secret key absent from the whole response body. + """ + for user in self.non_admins: + resp = self._get(self.public_comp, user) + assert resp.status_code == 200 + for field in ('id', 'title', 'created_by', 'phases', 'leaderboards'): + assert field in resp.data, field + for field in CompetitionDetailSerializer.ADMIN_ONLY_FIELDS: + assert field not in resp.data, field + assert str(self.public_comp.secret_key) not in resp.content.decode() + + def test_non_admins_with_secret_key_do_not_see_admin_only_fields(self): + """ + A logged-out user and an unrelated user open an unpublished competition with its secret key. + Expects a 200 response without any admin-only field. + """ + for user in [None, self.other_user]: + resp = self._get(self.private_comp, user, secret_key=str(self.private_comp.secret_key)) + assert resp.status_code == 200 + for field in CompetitionDetailSerializer.ADMIN_ONLY_FIELDS: + assert field not in resp.data, field + + def test_admins_see_hidden_leaderboards(self): + """ + The creator, a collaborator and a superuser request a competition with a visible and a hidden leaderboard. + Expects both leaderboards in the response. + """ + for user in self.admins: + resp = self._get(self.public_comp, user) + leaderboard_ids = {lb['id'] for lb in resp.data['leaderboards']} + assert leaderboard_ids == {self.visible_leaderboard.id, self.hidden_leaderboard.id} + + def test_non_admins_do_not_see_hidden_leaderboards(self): + """ + Participants, an unrelated user and a logged-out user request a competition + with a visible and a hidden leaderboard. + Expects only the visible leaderboard in the response. + """ + for user in self.non_admins: + resp = self._get(self.public_comp, user) + leaderboard_ids = {lb['id'] for lb in resp.data['leaderboards']} + assert leaderboard_ids == {self.visible_leaderboard.id} + + # ---------- Queue ---------- + + def test_admins_see_only_queue_id_and_name(self): + """ + The creator and a collaborator (neither owns the queue) and a superuser request a competition + that uses someone else's queue. + Expects the queue to have only id and name. + """ + for user in self.admins: + resp = self._get(self.public_comp, user) + assert resp.status_code == 200 + assert resp.data['queue'] == {'id': self.queue.id, 'name': self.queue.name} - def test_competition_creator_who_owns_queue_does_not_see_queue_broker_details(self): + def test_queue_owner_sees_only_queue_id_and_name_on_own_competition(self): """ The queue owner requests the detail API of their own competition that uses their queue. - Expects a 200 response where the queue has only id and name, with no broker credentials or vhost. + Expects the queue to have only id and name. Queue owners can get the broker URL from the queues API instead. """ - self.client.login(username='queue_owner', password='queue_owner') - resp = self.client.get(self.queue_owner_comp_url) - self._assert_queue_has_no_sensitive_details(resp) + resp = self._get(self.queue_owner_comp, self.queue_owner) + assert resp.status_code == 200 + assert resp.data['queue'] == {'id': self.queue.id, 'name': self.queue.name} + + def test_non_admins_do_not_see_queue(self): + """ + Participants, an unrelated user and a logged-out user request a competition that uses a queue. + Expects no queue field in the response. + """ + for user in self.non_admins: + resp = self._get(self.public_comp, user) + assert resp.status_code == 200 + assert 'queue' not in resp.data + + def test_admins_see_null_queue_when_competition_has_no_queue(self): + """ + The creator requests a competition that doesn't use a custom queue. + Expects the queue field to be None. + """ + comp = CompetitionFactory(created_by=self.creator, queue=None, published=True) + resp = self._get(comp, self.creator) + assert resp.status_code == 200 + assert resp.data['queue'] is None class CompetitionListTests(APITestCase):