From b5d8d91ce7e20da3d0b284ead55592c8b052b160 Mon Sep 17 00:00:00 2001 From: Obada Haddad-Soussac <11889208+ObadaS@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:30:07 +0100 Subject: [PATCH 01/31] version bump (#2076) * version bump --------- Co-authored-by: Obada Haddad --- version.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.json b/version.json index 50761f057..db3859c1f 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { - "tag_name": "1.21.1", - "release_name": "1.21.1", - "published_at": "2025-12-04", - "html_url": "https://github.com/codalab/codabench/releases/tag/v1.21.1" + "tag_name": "1.22", + "release_name": "1.22", + "published_at": "2025-12-08", + "html_url": "https://github.com/codalab/codabench/releases/tag/v1.22" } From 9eb15b8d27874caa9f2999fc027f406ca083ff2a Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 9 Dec 2025 00:05:32 +0500 Subject: [PATCH 02/31] banned user functionality added --- src/apps/api/tests/test_banned_user.py | 48 ++++++++++++++++++ src/apps/profiles/admin.py | 4 +- .../migrations/0017_user_is_banned.py | 18 +++++++ src/apps/profiles/models.py | 3 ++ src/middleware.py | 14 +++++ src/settings/base.py | 3 +- src/static/img/banned.png | Bin 0 -> 7638 bytes src/templates/banned.html | 17 +++++++ 8 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/apps/api/tests/test_banned_user.py create mode 100644 src/apps/profiles/migrations/0017_user_is_banned.py create mode 100644 src/middleware.py create mode 100644 src/static/img/banned.png create mode 100644 src/templates/banned.html diff --git a/src/apps/api/tests/test_banned_user.py b/src/apps/api/tests/test_banned_user.py new file mode 100644 index 000000000..cf5c4176d --- /dev/null +++ b/src/apps/api/tests/test_banned_user.py @@ -0,0 +1,48 @@ +# tests/test_banned_user.py + +from django.test import TestCase, Client +from django.contrib.auth import get_user_model +from django.urls import reverse + +User = get_user_model() + + +class BlockBannedUsersMiddlewareTests(TestCase): + + def setUp(self): + self.client = Client() + + # Normal user (not banned) + self.user = User.objects.create_user( + username="normaluser", + email="normal@example.com", + password="password123", + is_banned=False + ) + + # Banned user + self.banned_user = User.objects.create_user( + username="banneduser", + email="banned@example.com", + password="password123", + is_banned=True + ) + + def test_banned_user_sees_banned_page(self): + """Banned user should see banned.html page""" + self.client.login(username="banneduser", password="password123") + + response = self.client.get(reverse("pages:home")) + + self.assertEqual(response.status_code, 403) + self.assertTemplateUsed(response, "banned.html") + self.assertContains(response, "You are banned", status_code=403) + + def test_normal_user_can_access_page(self): + """Normal user should access pages normally""" + self.client.login(username="normaluser", password="password123") + + response = self.client.get(reverse("pages:home")) + + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, "You are banned") diff --git a/src/apps/profiles/admin.py b/src/apps/profiles/admin.py index cb2c5bee3..eafc21cdf 100644 --- a/src/apps/profiles/admin.py +++ b/src/apps/profiles/admin.py @@ -8,8 +8,8 @@ class UserAdmin(admin.ModelAdmin): change_form_template = "admin/auth/user/change_form.html" change_list_template = "admin/auth/user/change_list.html" search_fields = ['username', 'email'] - list_filter = ['is_staff', 'is_superuser', 'is_deleted', 'is_bot'] - list_display = ['username', 'email', 'is_staff', 'is_superuser'] + list_filter = ['is_staff', 'is_superuser', 'is_deleted', 'is_bot', 'is_banned'] + list_display = ['username', 'email', 'is_staff', 'is_superuser', 'is_banned'] class DeletedUserAdmin(admin.ModelAdmin): diff --git a/src/apps/profiles/migrations/0017_user_is_banned.py b/src/apps/profiles/migrations/0017_user_is_banned.py new file mode 100644 index 000000000..0f266d30d --- /dev/null +++ b/src/apps/profiles/migrations/0017_user_is_banned.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2025-12-08 18:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0016_deleteduser_user_id'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_banned', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/apps/profiles/models.py b/src/apps/profiles/models.py index 3a660440d..694ba22db 100644 --- a/src/apps/profiles/models.py +++ b/src/apps/profiles/models.py @@ -107,6 +107,9 @@ class User(ChaHubSaveMixin, AbstractBaseUser, PermissionsMixin): is_deleted = models.BooleanField(default=False) deleted_at = models.DateTimeField(null=True, blank=True) + # Ban + is_banned = models.BooleanField(default=False) + def save(self, *args, **kwargs): self.slug = slugify(self.username, allow_unicode=True) super().save(*args, **kwargs) diff --git a/src/middleware.py b/src/middleware.py new file mode 100644 index 000000000..b51fda9be --- /dev/null +++ b/src/middleware.py @@ -0,0 +1,14 @@ +from django.shortcuts import render + + +class BlockBannedUsersMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + user = request.user + + if user.is_authenticated and user.is_banned: + return render(request, "banned.html", status=403) + + return self.get_response(request) diff --git a/src/settings/base.py b/src/settings/base.py index 9db7a91ec..b2268b579 100644 --- a/src/settings/base.py +++ b/src/settings/base.py @@ -76,7 +76,8 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 'corsheaders.middleware.CorsMiddleware', # BB - 'django.middleware.common.CommonMiddleware' + 'django.middleware.common.CommonMiddleware', + 'middleware.BlockBannedUsersMiddleware' ) ROOT_URLCONF = 'urls' diff --git a/src/static/img/banned.png b/src/static/img/banned.png new file mode 100644 index 0000000000000000000000000000000000000000..034e2db41305be7c6fc2bf02350df862db2fa0c6 GIT binary patch literal 7638 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
=*6ts9E#XAmVOM)paCTnH(2Qd zgp5U0EuGt7S}&3_nA-@ZKjxUALOX`H=*Qa4Ztz&7@`%S2d7x!kP<%&!-ymeT@SBkSpkX-91% zg~U_K7K$W*_Rf>sY)X))U=+{`f7IWa=0Ypm7 zpvo`$bNlCxTS8A&()a;$(hlmIyjw3msDsHy5>hv074gc_nb$+DmiA|zD*T&z5!OP1 zHR+SyD3vRI7_Z6!%LCstK{)P2)ufcq3?Ydqcm9UX=^(%KQurMk^78a2mK}c}%JAVW z<}hd58bF`*?^7;vlF$9jCfM|WlEK?}R^h-K5RwVvj(P98UYpXgEn2qTw~NUJLC;^; z!_XY@(HVeyJjiy$9k>g!B?bJ&P+@imFaKzs6d$t!>{b0_m0X!RfX};(CmOr>%-My10H3ILlD>mXCNsaF{b*6V$T$B{1KHKu?=G)VQKyrh$`~=#^ zQu<3i99v$HX48nG&ZF?VYi`vd5sG>Tigj`k*7Z4a|I}_MRh9E^^19dbS?|G5pBl>o zs>fa${tQ6Jb?wv!MT`J|MP*HsMIdj{8&QsbD3dZg_-28O=2mmm;4jOT54W>6uDimV ziIq_rqc4sE*MdD9sqdx(h+|4)-+XXCqu93bK)|uM3*(9|!yH}SPEAZj-IE?MAt9&w z*A0!9=r{D3yqw)yfLyCUXm=!$I8c&7%gAfq;)xzFDd^AMw%<`eJ$Qz-x3L7r(7Ik^ zSo20&n!FzF$K?mVW*>RL`YG@y###OnZI(|WLXk{Oq7P;iaW3^NlXXqE--Xtb1vYr~ z#b16=mVtCAfIuHayWTaX&DqulAJru&zq_)zx_L_qg&8Qiyl)H@sF?`cU@`(9sXz0Z z=JIsx4gquF#YO*COj9XxkecM#!4`!BBV=eskyS0;3gw5c<2b8|EAB(U1ZDbFLn8e) zo%Vx9+t(LTmjhC}xe)kWlHh58Xnq~OwyZnEXTxn?rO631R!tn`BL33N@Qb225U1KP ztpCCPGFCUPF66B4TdtmsMdYmBGU z;VQas4>95Oc7E5hk!}}?0;=sWb^6uAzYaqT^sWJfwZrt};P#&>1MOC-x{Z1gn=gE2 zI&145)_v!nO}481Vj`2=ahQY%qX8bC2oQcL1_dx_x7(#vJipV!A(Ya0br ze{Zu>(gq;2Jsqu`5uvJJ%JJ{>?%rN7k|0g`mNhgxHTqDovHh$<=RZfu6L3O{&A@n( zJBcj^Pq_L>xO5K)d1B@3TFBs>XFlSu5VxT`{Q{A~bAIsDG$}(-Naq=#u zMwf#EL>HjNK8i^1Ik|eW`Pf;{E2_rf4)dMTXq)BV-efyTZX);QYKkBVZH!!R#EiJ^ zE!zp-%EOQ{z>c3kyEp+r#US}%BUS8;499d#xkGe8rMB^P>GY5N3o8baVx{h6k<7Y5 zmNj;8C2k~aVecXqZZcNu2CvjrlIWfCC^4bzhY9d@90U1>y9?CRILA^;(_|QadkycX zBXCsrzN=cTU%>!pQ74-3S4x{e7vbvc_S5arsJ-ng)G=%0BmK!05<;BOyNT21tv&^~ z8CQXG2aYC{`gygxNDr_R+pmCXNAYja|di#_1fSjRMx0yEioczAQ@SSXXugs!uhs@ zw_~9r9To@hqA%*VTms%Zm(Q7+jO8~LpO{U^@Afsx1^SJxUzp`ROVHR4fmwP3o6_A{ zb7R;i)%HC10hqVDH%pobMMuRMQhrO%A`UkLA4y6Z!L z{~VJgI*{)4+>YCtwn|)Z%bv?H8Ov+`-iM!<+Nu7MY-R9kCw(KqfoHVw3~v!nj@eMO z<|NG99fMEQ0*8nqY=h{c@*hJig>T(^_4UxN?7BnCv43IKJ~(?-f1Jy3^d`R<5rFzT zpfu@N`9Ct7E95mkk4y6buzvGrem319+mj=Rm9*hcZnzcs%7B|cj;%h+cpgsf;N}Bm znPvk4GlRQ8&7WttJ{bYcDftKY5)%KY%T~SY2t{@(FesQhd;Hu@ZbM=uF&l=y*3^LX znt=R^|5-o+p7zOaG(awBex(VA5i_8pB>_#N!qyezp}Z=Iy9 zqA8;?y1$6@!Z_K*&%uN|=5UouL8GUWl|j0%{&FByq&2a!r&B%Vx$9mEl_VwP0Dn-N za@$E1;1n0KFN|Zie2TPicl{r9?=fRwCP;U?Sb%N0^g;kdMXlot4tpLwYcze2P$Ht9rASmToBe zNjN{@9C9+eC?fk^F(m^%2&-*uJ<$X<@5;WppFPL^L}fJN(6bBEHCT*zy0X2#M9%!g zf>h1+fdbWre`C26+V(X55(TCi=~>U3gdP;^4o$6C%r!gB1%cjOb-pzKNgXD5rPGCh z+1cG$tce{zAw@ak7%_<<|9UyIT)hz?LxDL=WPT9oPKH!{VGB2ZMiI4WlsprulGZr1 za%umhR)FF2@gpEIp}7KANpHi$438Nv!hiXw+h@Sy-gi>Ae4ybF$frI>zIzPPRY*GN zsNCADj{EIJ2nc^wRO_&sJnXS%+-a#b_j1o04If?C376O->gW5O0*1J<(WG$4@TT=uJRlkI2C^G|zAt9!}im#C_i6=P5eyoAFb>4-&v zf2-*PP3FZOAHOI6bFk1kA7mo^_xI1bxO)oo z`Bkd6t8+x`xKCTAHyyQ79wJ8u29lljqLdD{>UH zEm}r(WRH2Lhzl-@Kb`!J1A4DrALAaTbX3k#0gwFDDIM zR8FhD!Av~>>K|4KVVPyYu?I9I+#Sy;G9GW=2vLpFNOBG*Z^If5FiR(;O>*Qn!s}If zZ=-_PJyEsicR9Wbn<)V^y3H%E48Vjpj4^1$%FstORe&TL@c^B7*~_PS0Y8CKA$ip+ z$9Sl^yMFCOBJZ8!tRt#H*%C6+mptQT=-UG{Q+0T|4T7eX~4#prQ}Hy@9Qz&cu$AFtusc!-=z1+m;=<5gI3Jn^pF zQ*GWoH^8sD!r3GV>UaSXf_TfPx=Upw0?{qNpl+9{=3n&RxtdA5@*hlO(pPQ~G#=Vm zornurw!~^<6%lxZjsH;D27um~`KfAeNXo_EdJ?z=4qjqe1ryaBzY@h@2`2b@jco?3 z)1b<*oK|7m;R)gjbm^N~SjgEh_5bG?Up(Tob7L%9^Z0o4Y)7bymU8LScOm}+_PP8H literal 0 HcmV?d00001 diff --git a/src/templates/banned.html b/src/templates/banned.html new file mode 100644 index 000000000..5f4325e5d --- /dev/null +++ b/src/templates/banned.html @@ -0,0 +1,17 @@ + + + + + Banned + + + + +

You are banned from using Codabench

+

If this is an error, please file a bug report.

+ + From 63106165009201ede6cf13cd25c1a6c77d68c26b Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 9 Dec 2025 14:18:13 +0500 Subject: [PATCH 03/31] middleware updated to return different responses for normal page visits and api calls. Tests updated to cover the updates --- src/apps/api/tests/test_banned_user.py | 24 +++++++++++++++++++++--- src/middleware.py | 9 +++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/apps/api/tests/test_banned_user.py b/src/apps/api/tests/test_banned_user.py index cf5c4176d..81b06829f 100644 --- a/src/apps/api/tests/test_banned_user.py +++ b/src/apps/api/tests/test_banned_user.py @@ -29,9 +29,8 @@ def setUp(self): ) def test_banned_user_sees_banned_page(self): - """Banned user should see banned.html page""" + """Banned user visiting a normal page should see banned.html""" self.client.login(username="banneduser", password="password123") - response = self.client.get(reverse("pages:home")) self.assertEqual(response.status_code, 403) @@ -41,8 +40,27 @@ def test_banned_user_sees_banned_page(self): def test_normal_user_can_access_page(self): """Normal user should access pages normally""" self.client.login(username="normaluser", password="password123") - response = self.client.get(reverse("pages:home")) self.assertEqual(response.status_code, 200) self.assertNotContains(response, "You are banned") + + def test_banned_user_api_request_returns_json(self): + """Banned user hitting API should get JSON error, not HTML page""" + self.client.login(username="banneduser", password="password123") + response = self.client.get(reverse("user_quota")) + + self.assertEqual(response.status_code, 403) + self.assertEqual(response["Content-Type"], "application/json") + self.assertJSONEqual( + response.content, + {"error": "You are banned from using Codabench"} + ) + + def test_normal_user_api_access(self): + """Normal user should get valid API response""" + self.client.login(username="normaluser", password="password123") + response = self.client.get(reverse("user_quota")) + + self.assertNotEqual(response.status_code, 403) + self.assertEqual(response.status_code, 200) diff --git a/src/middleware.py b/src/middleware.py index b51fda9be..1905a0f2c 100644 --- a/src/middleware.py +++ b/src/middleware.py @@ -1,4 +1,5 @@ from django.shortcuts import render +from django.http import JsonResponse class BlockBannedUsersMiddleware: @@ -7,8 +8,12 @@ def __init__(self, get_response): def __call__(self, request): user = request.user - if user.is_authenticated and user.is_banned: - return render(request, "banned.html", status=403) + # For api paths return json response + # For normal paths show banned page + if request.path.startswith("/api/"): + return JsonResponse({"error": "You are banned from using Codabench"}, status=403) + else: + return render(request, "banned.html", status=403) return self.get_response(request) From 10abb4c4c8242ba6f8becd4e52f7361e2a08a875 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 9 Dec 2025 14:19:16 +0500 Subject: [PATCH 04/31] comment removed --- src/apps/api/tests/test_banned_user.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/apps/api/tests/test_banned_user.py b/src/apps/api/tests/test_banned_user.py index 81b06829f..5467753c9 100644 --- a/src/apps/api/tests/test_banned_user.py +++ b/src/apps/api/tests/test_banned_user.py @@ -1,5 +1,3 @@ -# tests/test_banned_user.py - from django.test import TestCase, Client from django.contrib.auth import get_user_model from django.urls import reverse From c19da2d167991ec17bbe7d59931a43ee1b9daf25 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 9 Dec 2025 17:14:21 +0500 Subject: [PATCH 05/31] docs updated for ban users --- .../Administrator-procedures.md | 9 +++++++++ .../_attachments/users-django-admin.png | Bin 0 -> 131016 bytes 2 files changed, 9 insertions(+) create mode 100644 documentation/docs/Developers_and_Administrators/_attachments/users-django-admin.png diff --git a/documentation/docs/Developers_and_Administrators/Administrator-procedures.md b/documentation/docs/Developers_and_Administrators/Administrator-procedures.md index a8b0c1083..216477ee4 100644 --- a/documentation/docs/Developers_and_Administrators/Administrator-procedures.md +++ b/documentation/docs/Developers_and_Administrators/Administrator-procedures.md @@ -118,6 +118,15 @@ Select it, select the `Delete selected users` action and click on `Go`: ![](_attachments/c0fdd7ff-0c46-4bae-b9e2-7d4e0b774ec2_17534366434447145.jpg) +#### Ban/Unban a user + +Go to `Users` in the `django admin`: + +![Django Admin Users](_attachments/users-django-admin.png) + +Search for user using the search bar, or use the filter on the right side. Click on the username of the user to open user details, scroll down to find `Is Banned`. Check/uncheck this option to toggle the banned status. + + ## RabbitMQ Management The RabbitMQ management tool allows you to see the status of various queues, virtual hosts, and jobs. By default, you can access it at: `http://:15672/`. The username/password is your RabbitMQ `.env` settings for username and password. The port is hard-set in `docker-compose.yml` to 15672, but you can always change this if needed. For more information, see: diff --git a/documentation/docs/Developers_and_Administrators/_attachments/users-django-admin.png b/documentation/docs/Developers_and_Administrators/_attachments/users-django-admin.png new file mode 100644 index 0000000000000000000000000000000000000000..8ddfb0ec452d9a348af40e913d29196af1b9e517 GIT binary patch literal 131016 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

od{x}kqro-GOZIDF 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

)=6VmR8Wv`b+6kT88O(7R6rG;O#F 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 literal 0 HcmV?d00001 From a1de8c2a1b5b187b5ff3bff407413d4e2cf0c1b8 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 17 Jun 2025 20:52:16 +0500 Subject: [PATCH 06/31] fix to run result sbmission(with copy to predictions dir) --- compute_worker/compute_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 8f01c5dca..9e484e981 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -611,7 +611,7 @@ async def _run_program_directory(self, program_dir, kind): Function responsible for running program directory Args: - - program_dir : can be either ingestion program or program/submission + - program_dir : can be either ingestion program or program(submission or scoring) - kind : either `program` or `ingestion` """ # If the directory doesn't even exist, move on From c4892cbebceeae5e660c8d97075662b10889fcc9 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 23 Jun 2025 19:40:22 +0500 Subject: [PATCH 07/31] chahub cleanup Rebased with develop --- .../0028_submission_organization.py | 2 +- .../migrations/0059_auto_20250623_1341.py | 77 +++++++++++ src/apps/competitions/models.py | 122 +----------------- .../migrations/0011_auto_20250623_1341.py | 29 +++++ src/apps/datasets/models.py | 33 +---- .../migrations/0003_auto_20191122_1942.py | 13 +- .../migrations/0017_auto_20250623_1341.py | 34 +++++ src/apps/profiles/models.py | 45 +------ src/apps/profiles/pipeline.py | 24 ---- src/apps/profiles/views.py | 10 -- .../migrations/0005_auto_20250623_1341.py | 45 +++++++ src/apps/tasks/models.py | 65 +--------- src/settings/base.py | 17 +-- src/settings/test.py | 4 - src/utils/oauth_backends.py | 31 ----- 15 files changed, 202 insertions(+), 349 deletions(-) create mode 100644 src/apps/competitions/migrations/0059_auto_20250623_1341.py create mode 100644 src/apps/datasets/migrations/0011_auto_20250623_1341.py create mode 100644 src/apps/profiles/migrations/0017_auto_20250623_1341.py delete mode 100644 src/apps/profiles/pipeline.py create mode 100644 src/apps/tasks/migrations/0005_auto_20250623_1341.py delete mode 100644 src/utils/oauth_backends.py diff --git a/src/apps/competitions/migrations/0028_submission_organization.py b/src/apps/competitions/migrations/0028_submission_organization.py index fe4e974fb..f617ef85d 100644 --- a/src/apps/competitions/migrations/0028_submission_organization.py +++ b/src/apps/competitions/migrations/0028_submission_organization.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('profiles', '0010_auto_20201230_0034'), + ('profiles', '0001_initial'), ('competitions', '0027_auto_20201219_1308'), ] diff --git a/src/apps/competitions/migrations/0059_auto_20250623_1341.py b/src/apps/competitions/migrations/0059_auto_20250623_1341.py new file mode 100644 index 000000000..235a0014c --- /dev/null +++ b/src/apps/competitions/migrations/0059_auto_20250623_1341.py @@ -0,0 +1,77 @@ +# Generated by Django 2.2.28 on 2025-06-23 13:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('competitions', '0058_phase_hide_prediction_output'), + ] + + operations = [ + migrations.RemoveField( + model_name='competition', + name='chahub_data_hash', + ), + migrations.RemoveField( + model_name='competition', + name='chahub_needs_retry', + ), + migrations.RemoveField( + model_name='competition', + name='chahub_timestamp', + ), + migrations.RemoveField( + model_name='competition', + name='deleted', + ), + migrations.RemoveField( + model_name='competitionparticipant', + name='chahub_data_hash', + ), + migrations.RemoveField( + model_name='competitionparticipant', + name='chahub_needs_retry', + ), + migrations.RemoveField( + model_name='competitionparticipant', + name='chahub_timestamp', + ), + migrations.RemoveField( + model_name='competitionparticipant', + name='deleted', + ), + migrations.RemoveField( + model_name='phase', + name='chahub_data_hash', + ), + migrations.RemoveField( + model_name='phase', + name='chahub_needs_retry', + ), + migrations.RemoveField( + model_name='phase', + name='chahub_timestamp', + ), + migrations.RemoveField( + model_name='phase', + name='deleted', + ), + migrations.RemoveField( + model_name='submission', + name='chahub_data_hash', + ), + migrations.RemoveField( + model_name='submission', + name='chahub_needs_retry', + ), + migrations.RemoveField( + model_name='submission', + name='chahub_timestamp', + ), + migrations.RemoveField( + model_name='submission', + name='deleted', + ), + ] diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index 15cb1846b..60fc0ac0b 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -4,7 +4,6 @@ import botocore.exceptions from django.conf import settings -from django.contrib.sites.models import Site from django.contrib.postgres.fields import JSONField from django.core.files.base import ContentFile from django.db import models @@ -14,7 +13,6 @@ from decimal import Decimal from celery_config import app, app_for_vhost -from chahub.models import ChaHubSaveMixin from leaderboards.models import SubmissionScore from profiles.models import User, Organization from utils.data import PathWrapper @@ -27,7 +25,7 @@ logger = logging.getLogger(__name__) -class Competition(ChaHubSaveMixin, models.Model): +class Competition(models.Model): COMPETITION = "competition" BENCHMARK = "benchmark" @@ -206,46 +204,6 @@ def update_phase_statuses(self): def get_absolute_url(self): return reverse('competitions:detail', kwargs={'pk': self.pk}) - @staticmethod - def get_chahub_endpoint(): - return "competitions/" - - def get_chahub_is_valid(self): - has_phases = self.phases.exists() - upload_finished = all([c.status == CompetitionCreationTaskStatus.FINISHED for c in - self.creation_statuses.all()]) if self.creation_statuses.exists() else True - return has_phases and upload_finished - - def get_whitelist(self): - return [ - 'remote_id', - 'participants', - 'phases', - 'published', - ] - - def get_chahub_data(self): - data = { - 'created_by': self.created_by.username, - 'creator_id': self.created_by.pk, - 'created_when': self.created_when.isoformat(), - 'title': self.title, - 'url': f'http://{Site.objects.get_current().domain}{self.get_absolute_url()}', - 'remote_id': self.pk, - 'published': self.published, - 'participants': [p.get_chahub_data() for p in self.participants.all()], - 'phases': [phase.get_chahub_data(send_competition_id=False) for phase in self.phases.all()], - } - start = getattr(self.phases.order_by('index').first(), 'start', None) - data['start'] = start.isoformat() if start is not None else None - end = getattr(self.phases.order_by('index').last(), 'end', None) - data['end'] = end.isoformat() if end is not None else None - if self.logo: - data['logo_url'] = self.logo.url - data['logo'] = self.logo.url - - return self.clean_private_data(data) - def make_logo_icon(self): if self.logo: # Read the content of the logo file @@ -318,7 +276,7 @@ def __str__(self): return f"pk: {self.pk} ({self.status})" -class Phase(ChaHubSaveMixin, models.Model): +class Phase(models.Model): PREVIOUS = "Previous" CURRENT = "Current" NEXT = "Next" @@ -388,30 +346,6 @@ def can_user_make_submissions(self, user): return False, 'Reached maximum allowed submissions for this phase' return True, None - @staticmethod - def get_chahub_endpoint(): - return 'phases/' - - def get_whitelist(self): - return ['remote_id', 'published', 'tasks', 'index', 'status', 'competition_remote_id'] - - def get_chahub_data(self, send_competition_id=True): - data = { - 'remote_id': self.pk, - 'published': self.published, - 'status': self.status, - 'index': self.index, - 'start': self.start.isoformat(), - 'end': self.end.isoformat() if self.end else None, - 'name': self.name, - 'description': self.description, - 'is_active': self.is_active, - 'tasks': [task.get_chahub_data() for task in self.tasks.all()] - } - if send_competition_id: - data['competition_remote_id'] = self.competition.pk - return self.clean_private_data(data) - @property def is_active(self): """ Returns true when this phase of the competition is on-going. """ @@ -488,7 +422,7 @@ def save(self, *args, **kwargs): return super().save(*args, **kwargs) -class Submission(ChaHubSaveMixin, models.Model): +class Submission(models.Model): NONE = "None" SUBMITTING = "Submitting" SUBMITTED = "Submitted" @@ -779,37 +713,8 @@ def on_leaderboard(self): on_leaderboard = bool(self.children.first().leaderboard) return on_leaderboard - @staticmethod - def get_chahub_endpoint(): - return "submissions/" - - def get_whitelist(self): - return [ - 'remote_id', - 'is_public', - 'competition', - 'phase_index', - 'data', - ] - - def get_chahub_data(self): - data = { - "remote_id": self.id, - "is_public": self.is_public, - "competition": self.phase.competition_id, - "phase_index": self.phase.index, - "owner": self.owner.id, - "participant_name": self.owner.username, - "submitted_at": self.created_when.isoformat(), - "data": self.data.get_chahub_data(), - } - return self.clean_private_data(data) - - def get_chahub_is_valid(self): - return self.status == self.FINISHED - -class CompetitionParticipant(ChaHubSaveMixin, models.Model): +class CompetitionParticipant(models.Model): UNKNOWN = 'unknown' DENIED = 'denied' APPROVED = 'approved' @@ -833,25 +738,6 @@ class Meta: def __str__(self): return f"({self.id}) - User: {self.user.username} in Competition: {self.competition.title}" - @staticmethod - def get_chahub_endpoint(): - return 'participants/' - - def get_whitelist(self): - return [ - 'remote_id', - 'competition_id' - ] - - def get_chahub_data(self): - data = { - 'remote_id': self.pk, - 'user': self.user.id, - 'status': self.status, - 'competition_id': self.competition_id - } - return self.clean_private_data(data) - def save(self, *args, **kwargs): # Determine if this is a new participant (no existing record in DB) is_new = self.pk is None diff --git a/src/apps/datasets/migrations/0011_auto_20250623_1341.py b/src/apps/datasets/migrations/0011_auto_20250623_1341.py new file mode 100644 index 000000000..ef8022e4f --- /dev/null +++ b/src/apps/datasets/migrations/0011_auto_20250623_1341.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.28 on 2025-06-23 13:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('datasets', '0010_auto_20250218_1100'), + ] + + operations = [ + migrations.RemoveField( + model_name='data', + name='chahub_data_hash', + ), + migrations.RemoveField( + model_name='data', + name='chahub_needs_retry', + ), + migrations.RemoveField( + model_name='data', + name='chahub_timestamp', + ), + migrations.RemoveField( + model_name='data', + name='deleted', + ), + ] diff --git a/src/apps/datasets/models.py b/src/apps/datasets/models.py index 633ec5ac5..668e93977 100644 --- a/src/apps/datasets/models.py +++ b/src/apps/datasets/models.py @@ -3,14 +3,12 @@ import botocore.exceptions from django.conf import settings -from django.contrib.sites.models import Site from django.db import models from django.db.models import Q from django.urls import reverse from django.utils.timezone import now from decimal import Decimal -from chahub.models import ChaHubSaveMixin from utils.data import PathWrapper from utils.storage import BundleStorage from competitions.models import Competition @@ -20,7 +18,7 @@ logger = logging.getLogger(__name__) -class Data(ChaHubSaveMixin, models.Model): +class Data(models.Model): """Data models are unqiue based on name + created_by. If no name is given, then there is no uniqueness to enforce""" # It's useful to have these defaults map to the YAML names for these, like `scoring_program` @@ -113,32 +111,3 @@ def in_use(self): def __str__(self): return f'{self.name}({self.id})' - - @staticmethod - def get_chahub_endpoint(): - return "datasets/" - - def get_chahub_is_valid(self): - if not self.was_created_by_competition: - return self.upload_completed_successfully - else: - return True - - def get_whitelist(self): - return ['remote_id', 'is_public'] - - def get_chahub_data(self): - ssl = settings.SECURE_SSL_REDIRECT - site = Site.objects.get_current().domain - return self.clean_private_data({ - 'creator_id': self.created_by.id, - 'remote_id': self.pk, - 'created_by': str(self.created_by.username), - 'created_when': self.created_when.isoformat(), - 'name': self.name, - 'type': self.type, - 'description': self.description, - 'key': str(self.key), - 'is_public': self.is_public, - 'download_url': f'http{"s" if ssl else ""}://{site}{self.get_download_url()}' - }) diff --git a/src/apps/profiles/migrations/0003_auto_20191122_1942.py b/src/apps/profiles/migrations/0003_auto_20191122_1942.py index ec2474036..45518353a 100644 --- a/src/apps/profiles/migrations/0003_auto_20191122_1942.py +++ b/src/apps/profiles/migrations/0003_auto_20191122_1942.py @@ -1,7 +1,6 @@ # Generated by Django 2.1.11 on 2019-11-22 19:42 from django.db import migrations, models -import profiles.models class Migration(migrations.Migration): @@ -11,12 +10,12 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterModelManagers( - name='user', - managers=[ - ('objects', profiles.models.ChaHubUserManager()), - ], - ), + # migrations.AlterModelManagers( + # name='user', + # managers=[ + # ('objects', profiles.models.ChaHubUserManager()), + # ], + # ), migrations.AddField( model_name='user', name='deleted', diff --git a/src/apps/profiles/migrations/0017_auto_20250623_1341.py b/src/apps/profiles/migrations/0017_auto_20250623_1341.py new file mode 100644 index 000000000..e9601bfdb --- /dev/null +++ b/src/apps/profiles/migrations/0017_auto_20250623_1341.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.28 on 2025-06-23 13:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0016_deleteduser_user_id'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ], + ), + migrations.RemoveField( + model_name='user', + name='chahub_data_hash', + ), + migrations.RemoveField( + model_name='user', + name='chahub_needs_retry', + ), + migrations.RemoveField( + model_name='user', + name='chahub_timestamp', + ), + migrations.RemoveField( + model_name='user', + name='deleted', + ), + ] diff --git a/src/apps/profiles/models.py b/src/apps/profiles/models.py index 694ba22db..cd5ca6b74 100644 --- a/src/apps/profiles/models.py +++ b/src/apps/profiles/models.py @@ -1,9 +1,8 @@ import uuid -from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, UserManager +from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser from django.db import models from django.utils.timezone import now -from chahub.models import ChaHubSaveMixin from django.utils.text import slugify from utils.data import PathWrapper from django.urls import reverse @@ -25,14 +24,6 @@ ] -class ChaHubUserManager(UserManager): - def get_queryset(self): - return super().get_queryset().filter(deleted=False) - - def all_objects(self): - return super().get_queryset() - - class DeletedUser(models.Model): user_id = models.IntegerField(null=True, blank=True) # Store the same ID as in the User table username = models.CharField(max_length=255) @@ -43,7 +34,7 @@ def __str__(self): return f"{self.username} ({self.email})" -class User(ChaHubSaveMixin, AbstractBaseUser, PermissionsMixin): +class User(AbstractBaseUser, PermissionsMixin): # Social needs the below setting. Username is not really set to UID. USERNAME_FIELD = 'username' EMAIL_FIELD = 'email' @@ -100,9 +91,6 @@ class User(ChaHubSaveMixin, AbstractBaseUser, PermissionsMixin): # Robot submissions is_bot = models.BooleanField(default=False) - # Required for social auth and such to create users - objects = ChaHubUserManager() - # Soft deletion is_deleted = models.BooleanField(default=False) deleted_at = models.DateTimeField(null=True, blank=True) @@ -127,35 +115,6 @@ def __str__(self): def slug_url(self): return reverse('profiles:user_profile', args=[self.slug]) - @staticmethod - def get_chahub_endpoint(): - return "profiles/" - - def get_whitelist(self): - # all chahub data is ok to send - pass - - def clean_private_data(self, data): - # overriding this to filter out blacklist data from above, just to make _sure_ we don't send that info - return {k: v for k, v in data.items() if k not in PROFILE_DATA_BLACKLIST} - - def get_chahub_data(self): - data = { - 'email': self.email, - 'username': self.username, - 'remote_id': self.pk, - 'details': { - "is_active": self.is_active, - "last_login": self.last_login.isoformat() if self.last_login else None, - "date_joined": self.date_joined.isoformat() if self.date_joined else None, - } - } - return self.clean_private_data(data) - - def get_chahub_is_valid(self): - # By default, always push - return True - def get_used_storage_space(self, binary=False): """ Function to calculate storage used by a user diff --git a/src/apps/profiles/pipeline.py b/src/apps/profiles/pipeline.py deleted file mode 100644 index b14ea63ca..000000000 --- a/src/apps/profiles/pipeline.py +++ /dev/null @@ -1,24 +0,0 @@ -from profiles.models import GithubUserInfo - - -def user_details(user, **kwargs): - """Update user details using data from provider.""" - backend = kwargs.get('backend') - - if user: - if backend and backend.name == 'chahub': - if kwargs.get('details', {}).get('github_info'): - github_info = kwargs['details'].pop('github_info', None) - if github_info and github_info.get('uid'): - obj, created = GithubUserInfo.objects.update_or_create( - uid=github_info.pop('uid'), - defaults=github_info, - ) - user.github_info = obj - user.save() - if created: - print("New github user info created for user: {}".format(user.username)) - if not created: - print("We updated existing info for user: {}".format(user.username)) - else: - pass diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index 366fdf298..5c2c116bc 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -33,8 +33,6 @@ class LoginView(auth_views.LoginView): def get_context_data(self, *args, **kwargs): context = super(LoginView, self).get_context_data(*args, **kwargs) - # "http://localhost:8888/profiles/signup?next=http://localhost/social/login/chahub" - context['chahub_signup_url'] = "{}/profiles/signup?next={}/social/login/chahub".format(settings.SOCIAL_AUTH_CHAHUB_BASE_URL, settings.SITE_DOMAIN) return context @@ -199,10 +197,6 @@ def sign_up(request): return redirect('accounts:login') context = {} - context['chahub_signup_url'] = "{}/profiles/signup?next={}/social/login/chahub".format( - settings.SOCIAL_AUTH_CHAHUB_BASE_URL, - settings.SITE_DOMAIN - ) if request.method == 'POST': form = SignUpForm(request.POST) if form.is_valid(): @@ -266,10 +260,6 @@ def log_in(request): next = request.GET.get('next', None) context = {} - context['chahub_signup_url'] = "{}/profiles/signup?next={}/social/login/chahub".format( - settings.SOCIAL_AUTH_CHAHUB_BASE_URL, - settings.SITE_DOMAIN - ) if request.method == 'POST': form = LoginForm(request.POST) diff --git a/src/apps/tasks/migrations/0005_auto_20250623_1341.py b/src/apps/tasks/migrations/0005_auto_20250623_1341.py new file mode 100644 index 000000000..a4ffc9cf2 --- /dev/null +++ b/src/apps/tasks/migrations/0005_auto_20250623_1341.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.28 on 2025-06-23 13:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0004_task_shared_with'), + ] + + operations = [ + migrations.RemoveField( + model_name='solution', + name='chahub_data_hash', + ), + migrations.RemoveField( + model_name='solution', + name='chahub_needs_retry', + ), + migrations.RemoveField( + model_name='solution', + name='chahub_timestamp', + ), + migrations.RemoveField( + model_name='solution', + name='deleted', + ), + migrations.RemoveField( + model_name='task', + name='chahub_data_hash', + ), + migrations.RemoveField( + model_name='task', + name='chahub_needs_retry', + ), + migrations.RemoveField( + model_name='task', + name='chahub_timestamp', + ), + migrations.RemoveField( + model_name='task', + name='deleted', + ), + ] diff --git a/src/apps/tasks/models.py b/src/apps/tasks/models.py index c93d2b60a..e54ed75e6 100644 --- a/src/apps/tasks/models.py +++ b/src/apps/tasks/models.py @@ -4,10 +4,8 @@ from django.db import models from django.utils.timezone import now -from chahub.models import ChaHubSaveMixin - -class Task(ChaHubSaveMixin, models.Model): +class Task(models.Model): name = models.CharField(max_length=256) description = models.TextField(null=True, blank=True) key = models.UUIDField(default=uuid.uuid4, blank=True, unique=True) @@ -31,43 +29,8 @@ def _validated(self): # TODO: Should only include submissions that are successful, not any! return self.solutions.filter(md5__in=self.phases.values_list('submissions__md5', flat=True)).exists() - @staticmethod - def get_chahub_endpoint(): - return 'tasks/' - - def get_whitelist(self): - return [ - 'remote_id', - 'is_public', - 'solutions', - 'ingestion_program', - 'input_data', - 'reference_data', - 'scoring_program', - ] - - def get_chahub_data(self, include_solutions=True): - data = { - 'remote_id': self.pk, - 'created_by': self.created_by.username, - 'creator_id': self.created_by.pk, - 'created_when': self.created_when.isoformat(), - 'name': self.name, - 'description': self.description, - 'key': str(self.key), - 'is_public': self.is_public, - 'ingestion_program': self.ingestion_program.get_chahub_data() if self.ingestion_program else None, - 'input_data': self.input_data.get_chahub_data() if self.input_data else None, - 'ingestion_only_during_scoring': self.ingestion_only_during_scoring, - 'reference_data': self.reference_data.get_chahub_data() if self.reference_data else None, - 'scoring_program': self.scoring_program.get_chahub_data() if self.scoring_program else None, - } - if include_solutions: - data['solutions'] = [solution.get_chahub_data(include_tasks=False) for solution in self.solutions.all()] - return self.clean_private_data(data) - -class Solution(ChaHubSaveMixin, models.Model): +class Solution(models.Model): name = models.CharField(max_length=256) description = models.TextField(null=True, blank=True) key = models.UUIDField(default=uuid.uuid4, blank=True, unique=True) @@ -79,27 +42,3 @@ class Solution(ChaHubSaveMixin, models.Model): def __str__(self): return f"Solution - {self.name} - ({self.id})" - - @staticmethod - def get_chahub_endpoint(): - return 'solutions/' - - def get_whitelist(self): - return [ - 'remote_id', - 'is_public', - 'data', - 'tasks', - ] - - def get_chahub_data(self, include_tasks=True): - data = { - 'remote_id': self.pk, - 'name': self.name, - 'description': self.description, - 'key': str(self.key), - 'data': self.data.get_chahub_data(), # Todo: Make sure data is public if solution is public - } - if include_tasks: - data['tasks'] = [task.get_chahub_data(include_solutions=False) for task in self.tasks.all()] - return self.clean_private_data(data) diff --git a/src/settings/base.py b/src/settings/base.py index b2268b579..7f6b0e79a 100644 --- a/src/settings/base.py +++ b/src/settings/base.py @@ -49,12 +49,11 @@ 'redis', ) OUR_APPS = ( - 'chahub', + 'profiles', 'analytics', 'competitions', 'datasets', 'pages', - 'profiles', 'leaderboards', 'tasks', 'commands', @@ -121,7 +120,6 @@ # ============================================================================= AUTHENTICATION_BACKENDS = ( 'social_core.backends.github.GithubOAuth2', - 'utils.oauth_backends.ChahubOAuth2', 'django.contrib.auth.backends.ModelBackend', 'django_su.backends.SuBackend', 'profiles.backends.EmailAuthenticationBackend', @@ -136,7 +134,6 @@ 'social_core.pipeline.social_auth.load_extra_data', # 'social_core.pipeline.user.user_details', 'social_core.pipeline.social_auth.associate_by_email', - 'profiles.pipeline.user_details', ) # Github @@ -144,11 +141,6 @@ SOCIAL_AUTH_GITHUB_SECRET = os.environ.get('SOCIAL_AUTH_GITHUB_SECRET') SOCIAL_AUTH_GITHUB_SCOPE = ['user'] -# Codalab Example settings -SOCIAL_AUTH_CHAHUB_BASE_URL = os.environ.get('SOCIAL_AUTH_CHAHUB_BASE_URL', 'asdfasdfasfd') -SOCIAL_AUTH_CHAHUB_KEY = os.environ.get('SOCIAL_AUTH_CHAHUB_KEY', 'asdfasdfasfd') -SOCIAL_AUTH_CHAHUB_SECRET = os.environ.get('SOCIAL_AUTH_CHAHUB_SECRET', 'asdfasdfasfdasdfasdf') - # Generic SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy' SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage' @@ -460,13 +452,6 @@ def setup_celery_logging(**kwargs): DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'Codabench ') SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'noreply@codabench.org') -# ============================================================================= -# Chahub -# ============================================================================= -CHAHUB_API_URL = os.environ.get('CHAHUB_API_URL') -CHAHUB_API_KEY = os.environ.get('CHAHUB_API_KEY') -CHAHUB_PRODUCER_ID = os.environ.get('CHAHUB_PRODUCER_ID') - # Django-Su (User impersonation) SU_LOGIN_CALLBACK = 'profiles.admin.su_login_callback' diff --git a/src/settings/test.py b/src/settings/test.py index 21c5ababc..ec5963f3d 100644 --- a/src/settings/test.py +++ b/src/settings/test.py @@ -24,7 +24,3 @@ SELENIUM_HOSTNAME = os.environ.get("SELENIUM_HOSTNAME", "localhost") IS_TESTING = True - -CHAHUB_API_URL = None -CHAHUB_API_KEY = None -CHAHUB_PRODUCER_ID = None diff --git a/src/utils/oauth_backends.py b/src/utils/oauth_backends.py deleted file mode 100644 index c4a766194..000000000 --- a/src/utils/oauth_backends.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.conf import settings -from social_core.backends.oauth import BaseOAuth2 - - -BASE_URL = settings.SOCIAL_AUTH_CHAHUB_BASE_URL - - -class ChahubOAuth2(BaseOAuth2): - """Chahub OAuth authentication backend""" - name = 'chahub' - API_URL = '{}/api/v1/'.format(BASE_URL) - AUTHORIZATION_URL = '{}/oauth/authorize/'.format(BASE_URL) - ACCESS_TOKEN_URL = '{}/oauth/token/'.format(BASE_URL) - ACCESS_TOKEN_METHOD = 'POST' - ID_KEY = 'id' - - def get_user_id(self, details, response): - return details.get('id') - - def get_user_details(self, response): - access_token = response['access_token'] - my_profile_url = "{}my_profile/".format(self.API_URL) - data = self.get_json(my_profile_url, headers={'Authorization': 'Bearer {}'.format(access_token)}) - - return { - 'username': data.get('username'), - 'email': data.get('email'), - 'name': data.get('name', ''), - 'id': data.get('id'), - 'github_info': data.get('github_info', {}) - } From a26b10d53539734392832579d35b81bde9d32310 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 23 Jun 2025 19:43:47 +0500 Subject: [PATCH 08/31] ompute worker code reverted Rebased with develop --- compute_worker/compute_worker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 9e484e981..4d5c13f2a 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -611,7 +611,7 @@ async def _run_program_directory(self, program_dir, kind): Function responsible for running program directory Args: - - program_dir : can be either ingestion program or program(submission or scoring) + - program_dir : can be either ingestion program or program/submission - kind : either `program` or `ingestion` """ # If the directory doesn't even exist, move on @@ -632,9 +632,11 @@ async def _run_program_directory(self, program_dir, kind): logger.warning( "Program directory missing metadata, assuming it's going to be handled by ingestion" ) + # Copy submission files into prediction output # This is useful for results submissions but wrongly uses storage shutil.copytree(program_dir, self.output_dir) + return else: raise SubmissionException("Program directory missing 'metadata.yaml/metadata'") From 89a3d62c3da80a604c35d2315c206dcccc4e1e8e Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 23 Jun 2025 19:44:57 +0500 Subject: [PATCH 09/31] fixed migration --- .../competitions/migrations/0028_submission_organization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/competitions/migrations/0028_submission_organization.py b/src/apps/competitions/migrations/0028_submission_organization.py index f617ef85d..fe4e974fb 100644 --- a/src/apps/competitions/migrations/0028_submission_organization.py +++ b/src/apps/competitions/migrations/0028_submission_organization.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('profiles', '0001_initial'), + ('profiles', '0010_auto_20201230_0034'), ('competitions', '0027_auto_20201219_1308'), ] From 1405c508a990c09584d9e5c96a426fca47d13b31 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 23 Jun 2025 22:31:34 +0500 Subject: [PATCH 10/31] removed chahub app, updated UserManager rebased with develop --- src/apps/chahub/__init__.py | 0 src/apps/chahub/models.py | 143 --------------- src/apps/chahub/tasks.py | 164 ------------------ src/apps/chahub/tests/__init__.py | 0 src/apps/chahub/tests/test_chahub_mixin.py | 98 ----------- src/apps/chahub/tests/test_chahub_tasks.py | 37 ---- src/apps/chahub/tests/utils.py | 42 ----- src/apps/chahub/utils.py | 36 ---- .../migrations/0003_auto_20191122_1942.py | 13 +- .../migrations/0018_auto_20250623_1719.py | 20 +++ src/apps/profiles/models.py | 13 +- 11 files changed, 39 insertions(+), 527 deletions(-) delete mode 100644 src/apps/chahub/__init__.py delete mode 100644 src/apps/chahub/models.py delete mode 100644 src/apps/chahub/tasks.py delete mode 100644 src/apps/chahub/tests/__init__.py delete mode 100644 src/apps/chahub/tests/test_chahub_mixin.py delete mode 100644 src/apps/chahub/tests/test_chahub_tasks.py delete mode 100644 src/apps/chahub/tests/utils.py delete mode 100644 src/apps/chahub/utils.py create mode 100644 src/apps/profiles/migrations/0018_auto_20250623_1719.py diff --git a/src/apps/chahub/__init__.py b/src/apps/chahub/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/apps/chahub/models.py b/src/apps/chahub/models.py deleted file mode 100644 index dc49920bb..000000000 --- a/src/apps/chahub/models.py +++ /dev/null @@ -1,143 +0,0 @@ -import hashlib -import json - -from django.conf import settings -from django.db import models - -from chahub.tasks import send_to_chahub, delete_from_chahub - -import logging -logger = logging.getLogger(__name__) - - -class ChaHubModelManager(models.Manager): - def get_queryset(self): - return super().get_queryset().filter(deleted=False) - - def all_objects(self): - return super().get_queryset() - - -class ChaHubSaveMixin(models.Model): - """Helper mixin for saving model data to ChaHub. - - To use: - 1) Override `get_chahub_endpoint()` to return the endpoint on ChaHub API for this model - 2) Override `get_chahub_data()` to return a dictionary to send to ChaHub - 3) Override `get_whitelist()` to return a whitelist of fields to send to ChaHub if obj not public - 4) Be sure to call `self.clean_private_data()` inside `get_chahub_data` - 5) Override `get_chahub_is_valid()` to return True/False on whether or not the object is ready to send to ChaHub - 6) Data is sent on `save()` and `chahub_timestamp` timestamp is set - - To update remove the `chahub_timestamp` timestamp and call `save()`""" - # Timestamp set whenever a successful update happens - chahub_timestamp = models.DateTimeField(null=True, blank=True) - - # A hash of the last json information that was sent to avoid sending duplicate information - chahub_data_hash = models.TextField(null=True, blank=True) - - # If sending to chahub fails, we may need a retry. Signal that by setting this attribute to True - chahub_needs_retry = models.BooleanField(default=False) - - # Set to true if celery attempt at deletion does not get a 204 resp from chahub, so we can retry later - deleted = models.BooleanField(default=False) - - objects = ChaHubModelManager() - - class Meta: - abstract = True - - @property - def app_label(self): - return f'{self.__class__._meta.app_label}.{self.__class__.__name__}' - - def get_whitelist(self): - """Override this to set the return the whitelisted fields for private data - Example: - return ['remote_id', 'is_public'] - """ - raise NotImplementedError() - - # ------------------------------------------------------------------------- - # METHODS TO OVERRIDE WHEN USING THIS MIXIN! - # ------------------------------------------------------------------------- - @staticmethod - def get_chahub_endpoint(): - """Override this to return the endpoint URL for this resource - - Example: - # If the endpoint is chahub.org/api/v1/competitions/ then... - return "competitions/" - """ - raise NotImplementedError() - - def get_chahub_data(self): - """Override this to return a dictionary with data to send to chahub - - Example: - return {"name": self.name} - """ - raise NotImplementedError() - - def get_chahub_is_valid(self): - """Override this to validate the specific model before it's sent - - Example: - return comp.is_published - """ - # By default, always push - return True - - def clean_private_data(self, data): - """Override this to clean up any data that should not be sent to chahub if the object is not public""" - if hasattr(self, 'is_public'): - public = self.is_public - elif hasattr(self, 'published'): - public = self.published - else: - # assume data is good to push to chahub if there is no field saying otherwise - public = True - if not public: - for key in data.keys(): - if key not in self.get_whitelist(): - data[key] = None - return data - - # Regular methods - def save(self, send=True, *args, **kwargs): - # We do a save here to give us an ID for generating URLs and such - super().save(*args, **kwargs) - - # making sure get whitelist was implemented, making sure we don't send to chahub without cleaning our data - self.get_whitelist() - - if getattr(settings, 'IS_TESTING', False) and not getattr(settings, 'PYTEST_FORCE_CHAHUB', False): - # For tests let's just assume Chahub isn't available - # We can mock proper responses - return None - - # Make sure we're not sending these in tests - if settings.CHAHUB_API_URL and send: - is_valid = self.get_chahub_is_valid() - logger.info(f"ChaHub :: {self.__class__.__name__}({self.pk}) is_valid = {is_valid}") - - if is_valid: - data = [self.clean_private_data(self.get_chahub_data())] - - data_hash = hashlib.md5(json.dumps(data).encode('utf-8')).hexdigest() - # Send to chahub if we haven't yet, we have new data - if not self.chahub_timestamp or self.chahub_data_hash != data_hash: - send_to_chahub.apply_async((self.app_label, self.pk, data, data_hash)) - elif self.chahub_needs_retry: - # This is NOT valid but also marked as need retry, unmark need retry until this is valid again - logger.warning('ChaHub :: This is invalid but marked for retry. Clearing retry until valid again.') - self.chahub_needs_retry = False - super().save() - - def delete(self, send=True, *args, **kwargs): - if settings.CHAHUB_API_URL and send: - self.deleted = True - self.save(send=False) - delete_from_chahub.apply_async((self.app_label, self.pk)) - else: - super().delete(*args, **kwargs) diff --git a/src/apps/chahub/tasks.py b/src/apps/chahub/tasks.py deleted file mode 100644 index 36dcfd643..000000000 --- a/src/apps/chahub/tasks.py +++ /dev/null @@ -1,164 +0,0 @@ -import json - -import requests -from django.utils import timezone - -from celery_config import app -from django.apps import apps -from django.conf import settings -from apps.chahub.utils import ChahubException -import logging -logger = logging.getLogger(__name__) - - -def _send(endpoint, data): - url = f"{settings.CHAHUB_API_URL}{endpoint}" - headers = { - 'Content-type': 'application/json', - 'X-CHAHUB-API-KEY': settings.CHAHUB_API_KEY, - } - logger.info(f"ChaHub :: Sending to ChaHub ({url}) the following data: \n{data}") - return requests.post(url=url, data=json.dumps(data), headers=headers) - - -def get_obj(app_label, pk, include_deleted=False): - Model = apps.get_model(app_label) - - try: - if include_deleted: - obj = Model.objects.all_objects().get(pk=pk) - else: - obj = Model.objects.get(pk=pk) - except Model.DoesNotExist: - raise ChahubException(f"Could not find {app_label} with pk: {pk}") - return obj - - -@app.task(queue='site-worker') -def send_to_chahub(app_label, pk, data, data_hash): - """ - Does a post request to the specified API endpoint on chahub with the inputted data. - """ - if not settings.CHAHUB_API_URL: - raise ChahubException("CHAHUB_API_URL env var required to send to Chahub") - if not settings.CHAHUB_API_KEY: - raise ChahubException("No ChaHub API Key provided") - - obj = get_obj(app_label, pk) - - try: - resp = _send(obj.get_chahub_endpoint(), data) - except requests.exceptions.RequestException: - resp = None - - if resp and resp.status_code in (200, 201): - logger.info(f"ChaHub :: Received response {resp.status_code} {resp.content}") - obj.chahub_timestamp = timezone.now() - obj.chahub_data_hash = data_hash - obj.chahub_needs_retry = False - else: - status = getattr(resp, 'status_code', 'N/A') - body = getattr(resp, 'content', 'N/A') - logger.info(f"ChaHub :: Error sending to chahub, status={status}, body={body}") - obj.chahub_needs_retry = True - obj.save(send=False) - - -@app.task(queue='site-worker') -def delete_from_chahub(app_label, pk): - if not settings.CHAHUB_API_URL: - raise ChahubException("CHAHUB_API_URL env var required to send to Chahub") - if not settings.CHAHUB_API_KEY: - raise ChahubException("No ChaHub API Key provided") - - obj = get_obj(app_label, pk, include_deleted=True) - - url = f"{settings.CHAHUB_API_URL}{obj.get_chahub_endpoint()}{pk}/" - logger.info(f"ChaHub :: Sending to ChaHub ({url}) delete message") - - headers = {'X-CHAHUB-API-KEY': settings.CHAHUB_API_KEY} - - try: - resp = requests.delete(url=url, headers=headers) - except requests.exceptions.RequestException: - resp = None - - if resp and resp.status_code == 204: - logger.info(f"ChaHub :: Received response {resp.status_code} {resp.content}") - obj.delete(send=False) - else: - status = getattr(resp, 'status_code', 'N/A') - body = getattr(resp, 'content', 'N/A') - logger.error(f"ChaHub :: Error sending to chahub, status={status}, body={body}") - obj.chahub_needs_retry = True - obj.save(send=False) - - -def batch_send_to_chahub(model, limit=None, retry_only=False): - qs = model.objects.all() - if retry_only: - qs = qs.filter(chahub_needs_retry=True) - if limit is not None: - qs = qs[:limit] - - endpoint = model.get_chahub_endpoint() - data = [obj.get_chahub_data() for obj in qs if obj.get_chahub_is_valid()] - if not data: - logger.warning(f'Nothing to send to Chahub at {endpoint}') - return - try: - logger.info(f"Sending all data to Chahub at {endpoint}") - resp = _send(endpoint=endpoint, data=data) - logger.info(f"Response Status Code: {resp.status_code}") - if resp.status_code != 201: - logger.warning(f'ChaHub Response Content: {resp.content}') - except ChahubException: - logger.warning("There was a problem reaching Chahub. Retry again later") - - -def chahub_is_up(): - if not settings.CHAHUB_API_URL: - return False - - logger.info("Checking whether ChaHub is online before sending retries") - try: - response = requests.get(settings.CHAHUB_API_URL) - if response.ok: - logger.info("ChaHub is online") - return True - else: - logger.warning("Bad Status from ChaHub") - return False - except requests.exceptions.RequestException: - # This base exception works for HTTP errors, Connection errors, etc. - logger.error("Request Exception trying to access ChaHub") - return False - - -def get_chahub_models(): - from chahub.models import ChaHubSaveMixin - return ChaHubSaveMixin.__subclasses__() - - -@app.task(queue='site-worker') -def do_chahub_retries(limit=None): - if not chahub_is_up(): - return - chahub_models = get_chahub_models() - logger.info(f'Retrying for ChaHub models: {chahub_models}') - for model in chahub_models: - batch_send_to_chahub(model, retry_only=True, limit=limit) - obj_to_be_deleted = model.objects.all_objects().filter(deleted=True) - if limit is not None: - obj_to_be_deleted = obj_to_be_deleted[:limit] - for obj in obj_to_be_deleted: - # TODO: call celery task here, instead of abstracting. - obj.delete() - - -@app.task(queue='site-worker') -def send_everything_to_chahub(limit=None): - if not chahub_is_up(): - return - for model in get_chahub_models(): - batch_send_to_chahub(model, limit=limit) diff --git a/src/apps/chahub/tests/__init__.py b/src/apps/chahub/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/apps/chahub/tests/test_chahub_mixin.py b/src/apps/chahub/tests/test_chahub_mixin.py deleted file mode 100644 index 7b408e75b..000000000 --- a/src/apps/chahub/tests/test_chahub_mixin.py +++ /dev/null @@ -1,98 +0,0 @@ -from chahub.tests.utils import ChaHubTestCase -from competitions.models import Submission -from factories import UserFactory, CompetitionFactory, DataFactory, SubmissionFactory, PhaseFactory, \ - CompetitionParticipantFactory -from profiles.models import User - - -class SubmissionMixinTests(ChaHubTestCase): - def setUp(self): - self.user = UserFactory() - self.comp = CompetitionFactory(published=True) - self.participant = CompetitionParticipantFactory(user=self.user, competition=self.comp) - self.phase = PhaseFactory(competition=self.comp) - self.data = DataFactory() - # Calling this after initial setup so we don't turn on FORCE_CHAHUB and try and send all our setup objects - super().setUp() - self.submission = SubmissionFactory.build( - owner=self.user, - phase=self.phase, - data=self.data, - participant=self.participant, - status='Finished', - is_public=True, - leaderboard=None - ) - - def test_submission_save_sends_to_chahub(self): - resp = self.mock_chahub_save(self.submission) - assert resp.called - - def test_submission_save_not_sending_duplicate_data(self): - resp1 = self.mock_chahub_save(self.submission) - assert resp1.called - self.submission = Submission.objects.get(id=self.submission.id) - resp2 = self.mock_chahub_save(self.submission) - assert not resp2.called - - def test_submission_save_sends_updated_data(self): - resp1 = self.mock_chahub_save(self.submission) - assert resp1.called - self.phase.index += 1 - resp2 = self.mock_chahub_save(self.submission) - assert resp2.called - - # def test_invalid_submission_not_sent(self): - # self.submission.status = "Running" - # self.submission.is_public = False - # resp1 = self.mock_chahub_save(self.submission) - # assert not resp1.called - # self.submission = Submission.objects.get(id=self.submission.id) - # self.submission.status = "Finished" - # resp2 = self.mock_chahub_save(self.submission) - # assert resp2.called - - # def test_retrying_invalid_submission_wont_retry_again(self): - # self.submission.status = "Running" - # self.submission.chahub_needs_retry = True - # resp = self.mock_chahub_save(self.submission) - # assert not resp.called - # assert not Submission.objects.get(id=self.submission.id).chahub_needs_retry - - def test_valid_submission_marked_for_retry_sent_and_needs_retry_unset(self): - # Mark submission for retry - self.submission.chahub_needs_retry = True - resp = self.mock_chahub_save(self.submission) - assert resp.called - assert not Submission.objects.get(id=self.submission.id).chahub_needs_retry - - -class ProfileMixinTests(ChaHubTestCase): - def setUp(self): - self.user = UserFactory.build(username='admin') # create a user but don't save until later in the mock - super().setUp() - - def test_profile_save_not_sending_on_blacklisted_data_update(self): - resp1 = self.mock_chahub_save(self.user) - assert resp1.called - self.user = User.objects.get(id=self.user.id) - self.user.password = 'this_is_different' # Not using user.set_password() to control when the save happens - resp2 = self.mock_chahub_save(self.user) - assert not resp2.called - - -class CompetitionMixinTests(ChaHubTestCase): - def setUp(self): - self.comp = CompetitionFactory(published=False) - PhaseFactory(competition=self.comp) - super().setUp() - - def test_unpublished_comp_doesnt_send_private_data(self): - resp = self.mock_chahub_save(self.comp) - # Gross traversal through call args to get the data passed to _send - assert resp.called - data = resp.call_args[0][1][0] - whitelist = self.comp.get_whitelist() - for key, value in data.items(): - if key not in whitelist: - assert value is None diff --git a/src/apps/chahub/tests/test_chahub_tasks.py b/src/apps/chahub/tests/test_chahub_tasks.py deleted file mode 100644 index cd8e6836e..000000000 --- a/src/apps/chahub/tests/test_chahub_tasks.py +++ /dev/null @@ -1,37 +0,0 @@ -from chahub.tests.utils import ChaHubTestCase -from factories import UserFactory, CompetitionFactory, DataFactory, SubmissionFactory, PhaseFactory, \ - CompetitionParticipantFactory - - -class ChaHubDoRetriesTests(ChaHubTestCase): - def setUp(self): - for _ in range(5): - user = UserFactory(chahub_needs_retry=True) - comp = CompetitionFactory(chahub_needs_retry=True, published=True) - participant = CompetitionParticipantFactory(competition=comp, user=user, status='approved') - phase = PhaseFactory(competition=comp) - DataFactory(chahub_needs_retry=True, is_public=True, upload_completed_successfully=True) - SubmissionFactory( - chahub_needs_retry=True, - status="Finished", - phase=phase, - is_public=True, - participant=participant - ) - super().setUp() - - def test_do_retries_picks_up_all_expected_items(self): - resp = self.mock_retries() - # Should call once each for Users, Comps, Datasets, Submissions - assert resp.call_count == 4 - for call in resp.call_args_list: - # Should get passed a batch of data that is 5 long - assert len(call[1]['data']) == 5 - - def test_do_retries_limit_will_limit_number_of_retries(self): - resp = self.mock_retries(limit=2) - # Should call once each for Users, Comps, Datasets, Submissions - assert resp.call_count == 4 - for call in resp.call_args_list: - # Should get passed a batch of data that is 2 long, matching the limit - assert len(call[1]['data']) == 2 diff --git a/src/apps/chahub/tests/utils.py b/src/apps/chahub/tests/utils.py deleted file mode 100644 index 0a8501303..000000000 --- a/src/apps/chahub/tests/utils.py +++ /dev/null @@ -1,42 +0,0 @@ -from unittest import mock - -from django.conf import settings -from django.http.response import HttpResponseBase -from django.test import TestCase - -from chahub.tasks import do_chahub_retries - - -class ChaHubTestResponse(HttpResponseBase): - @property - def ok(self): - return self.status_code < 400 - - -class ChaHubTestCase(TestCase): - def setUp(self): - settings.PYTEST_FORCE_CHAHUB = True - # set the url to localhost for tests - settings.CHAHUB_API_URL = 'http://localhost/' - settings.CHAHUB_API_KEY = 'asdf' - - def tearDown(self): - settings.PYTEST_FORCE_CHAHUB = False - settings.CHAHUB_API_URL = None - - def mock_chahub_save(self, obj): - with mock.patch('chahub.tasks._send') as chahub_mock: - chahub_mock.return_value = ChaHubTestResponse(status=201) - chahub_mock.return_value.content = '' - obj.save() - return chahub_mock - - def mock_retries(self, limit=None): - with mock.patch('apps.chahub.tasks.requests.get') as chahub_get_mock: - # This checks that ChaHub is up, mock this so the task doesn't bail - chahub_get_mock.return_value = ChaHubTestResponse(status=200) - with mock.patch('chahub.tasks._send') as send_to_chahub_mock: - send_to_chahub_mock.return_value = ChaHubTestResponse(status=201) - send_to_chahub_mock.return_value.content = '' - do_chahub_retries(limit=limit) - return send_to_chahub_mock diff --git a/src/apps/chahub/utils.py b/src/apps/chahub/utils.py deleted file mode 100644 index 98a65962d..000000000 --- a/src/apps/chahub/utils.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.conf import settings - -import requests -import json - -import logging -logger = logging.getLogger(__name__) - - -class ChahubException(Exception): - pass - - -def send_to_chahub(endpoint, data): - """ - Does a post request to the specified API endpoint on chahub with the inputted data. - :param endpoint: String designating which API endpoint; IE: 'producers/' - :param data: Dictionary containing data we are sending away to the endpoint. - :return: - """ - if not endpoint: - raise ChahubException("No ChaHub API endpoint given") - if not settings.CHAHUB_API_URL: - raise ChahubException("CHAHUB_API_URL env var required to send to Chahub") - - url = f"{settings.CHAHUB_API_URL}{endpoint}" - - logger.info(f"ChaHub :: Sending to ChaHub ({url}) the following data: \n{data}") - try: - headers = { - 'Content-type': 'application/json', - 'X-CHAHUB-API-KEY': settings.CHAHUB_API_KEY, - } - return requests.post(url=url, data=json.dumps(data), headers=headers) - except requests.ConnectionError: - raise ChahubException('Connection Error with ChaHub') diff --git a/src/apps/profiles/migrations/0003_auto_20191122_1942.py b/src/apps/profiles/migrations/0003_auto_20191122_1942.py index 45518353a..2bfd8a549 100644 --- a/src/apps/profiles/migrations/0003_auto_20191122_1942.py +++ b/src/apps/profiles/migrations/0003_auto_20191122_1942.py @@ -1,6 +1,7 @@ # Generated by Django 2.1.11 on 2019-11-22 19:42 from django.db import migrations, models +import profiles.models class Migration(migrations.Migration): @@ -10,12 +11,12 @@ class Migration(migrations.Migration): ] operations = [ - # migrations.AlterModelManagers( - # name='user', - # managers=[ - # ('objects', profiles.models.ChaHubUserManager()), - # ], - # ), + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', profiles.models.CodabenchUserManager()), + ], + ), migrations.AddField( model_name='user', name='deleted', diff --git a/src/apps/profiles/migrations/0018_auto_20250623_1719.py b/src/apps/profiles/migrations/0018_auto_20250623_1719.py new file mode 100644 index 000000000..25d07fb3b --- /dev/null +++ b/src/apps/profiles/migrations/0018_auto_20250623_1719.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.28 on 2025-06-23 17:19 + +from django.db import migrations +import profiles.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0017_auto_20250623_1341'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', profiles.models.CodabenchUserManager()), + ], + ), + ] diff --git a/src/apps/profiles/models.py b/src/apps/profiles/models.py index cd5ca6b74..95ff20b34 100644 --- a/src/apps/profiles/models.py +++ b/src/apps/profiles/models.py @@ -1,6 +1,6 @@ import uuid -from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser +from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, UserManager from django.db import models from django.utils.timezone import now from django.utils.text import slugify @@ -24,6 +24,14 @@ ] +class CodabenchUserManager(UserManager): + def get_queryset(self): + return super().get_queryset().filter() + + def all_objects(self): + return super().get_queryset() + + class DeletedUser(models.Model): user_id = models.IntegerField(null=True, blank=True) # Store the same ID as in the User table username = models.CharField(max_length=255) @@ -91,6 +99,9 @@ class User(AbstractBaseUser, PermissionsMixin): # Robot submissions is_bot = models.BooleanField(default=False) + # Required for social auth and such to create users + objects = CodabenchUserManager() + # Soft deletion is_deleted = models.BooleanField(default=False) deleted_at = models.DateTimeField(null=True, blank=True) From c16947c1970a3a7e046ad187a476fd86f3a53e65 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 9 Dec 2025 21:45:51 +0500 Subject: [PATCH 11/31] compute worker spaces removed --- compute_worker/compute_worker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 4d5c13f2a..8f01c5dca 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -632,11 +632,9 @@ async def _run_program_directory(self, program_dir, kind): logger.warning( "Program directory missing metadata, assuming it's going to be handled by ingestion" ) - # Copy submission files into prediction output # This is useful for results submissions but wrongly uses storage shutil.copytree(program_dir, self.output_dir) - return else: raise SubmissionException("Program directory missing 'metadata.yaml/metadata'") From a5f708c89536a33fba4ec1bd21c9969de57373d5 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Wed, 10 Dec 2025 14:11:23 +0100 Subject: [PATCH 12/31] added new migrations (makemigrations --merge) --- ...011_auto_20250623_1341_0012_delete_datagroup.py | 14 ++++++++++++++ ..._0017_user_is_banned_0018_auto_20250623_1719.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/apps/datasets/migrations/0013_merge_0011_auto_20250623_1341_0012_delete_datagroup.py create mode 100644 src/apps/profiles/migrations/0019_merge_0017_user_is_banned_0018_auto_20250623_1719.py diff --git a/src/apps/datasets/migrations/0013_merge_0011_auto_20250623_1341_0012_delete_datagroup.py b/src/apps/datasets/migrations/0013_merge_0011_auto_20250623_1341_0012_delete_datagroup.py new file mode 100644 index 000000000..a36c2abd6 --- /dev/null +++ b/src/apps/datasets/migrations/0013_merge_0011_auto_20250623_1341_0012_delete_datagroup.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2 on 2025-12-10 13:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('datasets', '0011_auto_20250623_1341'), + ('datasets', '0012_delete_datagroup'), + ] + + operations = [ + ] diff --git a/src/apps/profiles/migrations/0019_merge_0017_user_is_banned_0018_auto_20250623_1719.py b/src/apps/profiles/migrations/0019_merge_0017_user_is_banned_0018_auto_20250623_1719.py new file mode 100644 index 000000000..3b9cf2e15 --- /dev/null +++ b/src/apps/profiles/migrations/0019_merge_0017_user_is_banned_0018_auto_20250623_1719.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2 on 2025-12-10 13:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0017_user_is_banned'), + ('profiles', '0018_auto_20250623_1719'), + ] + + operations = [ + ] From 7b83d3429161a2dcaf1f86e4cbead703ebf73635 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Fri, 8 Aug 2025 12:23:24 +0200 Subject: [PATCH 13/31] Changed domain name setting to not show example.com but the actual domain name defined in the .env --- src/apps/profiles/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index 5c2c116bc..c4bf6e6e7 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -94,7 +94,7 @@ def activateEmail(request, user, to_email): mail_subject = 'Activate your user account.' message = render_to_string('profiles/emails/template_activate_account.html', { 'username': user.username, - 'domain': get_current_site(request).domain, + 'domain': settings.DOMAIN_NAME, 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 'token': account_activation_token.make_token(user), 'protocol': 'https' if request.is_secure() else 'http' From 2376921ce2a98da447e12cebbb8ab34662c675b4 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sat, 28 Jun 2025 22:04:35 +0500 Subject: [PATCH 14/31] Submission delete API bug fixed. More restrictions added --- src/apps/api/views/submissions.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/apps/api/views/submissions.py b/src/apps/api/views/submissions.py index b14ea9050..90f9ff927 100644 --- a/src/apps/api/views/submissions.py +++ b/src/apps/api/views/submissions.py @@ -198,11 +198,27 @@ def create(self, request, *args, **kwargs): return super(SubmissionViewSet, self).create(request, *args, **kwargs) def destroy(self, request, *args, **kwargs): + """ + - If a user is owner of a submission and submission is not on the leaderboard, user can delete the submission using the delete API + - If a user is either super user or admin of the competition of the submission, user can delete the submission + - If user is neither owner nor admin, user cannot delete the submission + """ submission = self.get_object() - if request.user != submission.owner and not self.has_admin_permission(request.user, submission): - raise PermissionDenied("Cannot interact with submission you did not make") + is_owner = request.user == submission.owner + is_super_user_or_competition_admin = self.has_admin_permission(request.user, submission) + + # If user is neither owner nor super user/admin + # return permission denied + if not is_owner and not is_super_user_or_competition_admin: + raise PermissionDenied("You do not have permission to delete this submission!") + + # If user is owner but submission is on the leaderboard + # return permission denied + if is_owner and submission.leaderboard: + raise PermissionDenied("You cannot delete a leaderboard submission!") + # Otherwise, delete the submission self.perform_destroy(submission) return Response(status=status.HTTP_204_NO_CONTENT) From 7b68de9abf471389dd7bddf50da0ca6c059425d1 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sun, 29 Jun 2025 14:04:47 +0500 Subject: [PATCH 15/31] tests updated and fixed --- src/apps/api/tests/test_submissions.py | 35 ++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/apps/api/tests/test_submissions.py b/src/apps/api/tests/test_submissions.py index a12e3ef13..64fd0f0aa 100644 --- a/src/apps/api/tests/test_submissions.py +++ b/src/apps/api/tests/test_submissions.py @@ -22,6 +22,7 @@ def setUp(self): self.collaborator = UserFactory(username='collab', password='collab') self.comp = CompetitionFactory(created_by=self.creator, collaborators=[self.collaborator]) self.phase = PhaseFactory(competition=self.comp) + self.leaderboard = LeaderboardFactory() # Extra dummy user to test permissions, they shouldn't have access to many things self.other_user = UserFactory(username='other_user', password='other') @@ -41,7 +42,16 @@ def setUp(self): phase=self.phase, owner=self.participant, status=Submission.SUBMITTED, - secret='7df3600c-1234-5678-bbc8-bbe91f42d875' + secret='7df3600c-1234-5678-bbc8-bbe91f42d875', + leaderboard=None + ) + + # add submission with that is on the leaderboard + self.leaderboard_submission = SubmissionFactory( + phase=self.phase, + owner=self.participant, + status=Submission.SUBMITTED, + leaderboard=self.leaderboard ) def test_can_make_submission_checks_if_you_are_participant(self): @@ -95,27 +105,23 @@ def test_cannot_delete_submission_you_didnt_create(self): # As anonymous user resp = self.client.delete(url) assert resp.status_code == 403 - assert resp.data["detail"] == "Cannot interact with submission you did not make" + assert resp.data["detail"] == "You do not have permission to delete this submission!" # As regular user self.client.force_login(self.other_user) resp = self.client.delete(url) assert resp.status_code == 403 - assert resp.data["detail"] == "Cannot interact with submission you did not make" + assert resp.data["detail"] == "You do not have permission to delete this submission!" + def test_can_delete_submission_you_created(self): + url = reverse('submission-detail', args=(self.existing_submission.pk,)) # As user who made submission self.client.force_login(self.participant) resp = self.client.delete(url) assert resp.status_code == 204 assert not Submission.objects.filter(pk=self.existing_submission.pk).exists() - # As superuser (re-making submission since it has been destroyed) - self.existing_submission = SubmissionFactory( - phase=self.phase, - owner=self.participant, - status=Submission.SUBMITTED, - secret='7df3600c-1234-5678-90c8-bbe91f42d875' - ) + def test_super_user_can_delete_submission_you_created(self): url = reverse('submission-detail', args=(self.existing_submission.pk,)) self.client.force_login(self.superuser) @@ -123,6 +129,15 @@ def test_cannot_delete_submission_you_didnt_create(self): assert resp.status_code == 204 assert not Submission.objects.filter(pk=self.existing_submission.pk).exists() + def test_cannot_delete_leaderboard_submission_you_created(self): + url = reverse('submission-detail', args=(self.leaderboard_submission.pk,)) + + self.client.force_login(self.participant) + resp = self.client.delete(url) + + assert resp.status_code == 403 + assert resp.data["detail"] == "You cannot delete a leaderboard submission!" + def test_cannot_get_details_of_submission_unless_creator_collab_or_superuser(self): url = reverse('submission-get-details', args=(self.existing_submission.pk,)) From f1c9ddd9d148276237adccab87a88fd81c08d8d8 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 30 Jun 2025 08:29:26 +0500 Subject: [PATCH 16/31] missing condition added, new test added for missing condition --- src/apps/api/tests/test_submissions.py | 8 ++++++++ src/apps/api/views/submissions.py | 12 +++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/apps/api/tests/test_submissions.py b/src/apps/api/tests/test_submissions.py index 64fd0f0aa..f498b1608 100644 --- a/src/apps/api/tests/test_submissions.py +++ b/src/apps/api/tests/test_submissions.py @@ -129,6 +129,14 @@ def test_super_user_can_delete_submission_you_created(self): assert resp.status_code == 204 assert not Submission.objects.filter(pk=self.existing_submission.pk).exists() + def test_super_user_can_delete_leaderboard_submission_you_created(self): + url = reverse('submission-detail', args=(self.leaderboard_submission.pk,)) + + self.client.force_login(self.superuser) + resp = self.client.delete(url) + assert resp.status_code == 204 + assert not Submission.objects.filter(pk=self.leaderboard_submission.pk).exists() + def test_cannot_delete_leaderboard_submission_you_created(self): url = reverse('submission-detail', args=(self.leaderboard_submission.pk,)) diff --git a/src/apps/api/views/submissions.py b/src/apps/api/views/submissions.py index 90f9ff927..1b89ec6ae 100644 --- a/src/apps/api/views/submissions.py +++ b/src/apps/api/views/submissions.py @@ -199,23 +199,21 @@ def create(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): """ - - If a user is owner of a submission and submission is not on the leaderboard, user can delete the submission using the delete API - - If a user is either super user or admin of the competition of the submission, user can delete the submission - If user is neither owner nor admin, user cannot delete the submission + - If a user is not admin and is owner of a submission and submission is on the leaderboard, user cannot delete the submission + - In rest of the cases i.e. user is admin/super user or user is owner of the submisison and submission is not on the leaderboard, user can delete the submisison """ submission = self.get_object() is_owner = request.user == submission.owner is_super_user_or_competition_admin = self.has_admin_permission(request.user, submission) - # If user is neither owner nor super user/admin - # return permission denied + # If user is neither owner nor super user/admin return permission denied if not is_owner and not is_super_user_or_competition_admin: raise PermissionDenied("You do not have permission to delete this submission!") - # If user is owner but submission is on the leaderboard - # return permission denied - if is_owner and submission.leaderboard: + # If user is not admin, is owner and submission is on the leaderboard return permission denied + if not is_super_user_or_competition_admin and is_owner and submission.leaderboard: raise PermissionDenied("You cannot delete a leaderboard submission!") # Otherwise, delete the submission From bd99085bd1424c7db9e78c66d4f1b120c8e60dd8 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 7 Jul 2025 14:32:42 +0500 Subject: [PATCH 17/31] phase creation from ui with starting kit/public data bug fixed --- src/apps/api/views/competitions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index 09eeb2491..493a13ea7 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -227,9 +227,20 @@ def create(self, request, *args, **kwargs): leaderboard.is_valid() leaderboard.save() leaderboard_id = leaderboard["id"].value + + # Set leaderboard id, starting kit and public data for phases for phase in data['phases']: phase['leaderboard'] = leaderboard_id + try: + phase['public_data'] = Data.objects.filter(key=phase['public_data']['value'])[0].id + except TypeError: + phase['public_data'] = None + try: + phase['starting_kit'] = Data.objects.filter(key=phase['starting_kit']['value'])[0].id + except TypeError: + phase['starting_kit'] = None + serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) From d277af0c03008ac821e3e2a3203f329ff9474b45 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 13 Oct 2025 17:21:39 +0500 Subject: [PATCH 18/31] datasets api delete tests added --- src/apps/api/tests/test_datasets.py | 93 ++++++++++++++++++++++++++++- src/apps/api/views/datasets.py | 8 +-- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/apps/api/tests/test_datasets.py b/src/apps/api/tests/test_datasets.py index 5b52b4b95..b116184d7 100644 --- a/src/apps/api/tests/test_datasets.py +++ b/src/apps/api/tests/test_datasets.py @@ -3,7 +3,14 @@ from django.test import TestCase from rest_framework.test import APITestCase from datasets.models import Data -from factories import UserFactory, DataFactory +from factories import ( + UserFactory, + DataFactory, + CompetitionFactory, + PhaseFactory, + TaskFactory, + SubmissionFactory +) from utils.data import pretty_bytes, gb_to_bytes from unittest.mock import patch @@ -306,3 +313,87 @@ def test_cannot_create_dataset_unauthenticated(self): 'file_size': 1234, }) self.assertEqual(resp.status_code, 403) + + +class DatasetDeleteTests(APITestCase): + def setUp(self): + self.user = UserFactory(username='user', password='user') + self.other_user = UserFactory(username='other', password='other') + self.client.login(username='user', password='user') + + self.dataset1 = DataFactory(created_by=self.user, name='dataset1') + self.dataset2 = DataFactory(created_by=self.user, name='dataset2') + self.other_dataset = DataFactory(created_by=self.other_user, name='other_dataset') + + def test_delete_own_dataset_success(self): + """User can delete their own dataset.""" + url = reverse("data-detail", args=[self.dataset1.pk]) + resp = self.client.delete(url) + self.assertEqual(resp.status_code, 204) + self.assertFalse(Data.objects.filter(pk=self.dataset1.pk).exists()) + + def test_cannot_delete_others_dataset(self): + """User cannot delete someone else’s dataset.""" + url = reverse("data-detail", args=[self.other_dataset.pk]) + resp = self.client.delete(url) + self.assertEqual(resp.status_code, 404) + self.assertTrue(Data.objects.filter(pk=self.other_dataset.pk).exists()) + + def test_cannot_delete_dataset_in_use(self): + """If dataset is in use by a competition, it cannot be deleted.""" + # Set up in use dataset + in_use_dataset = DataFactory(type=Data.INPUT_DATA, created_by=self.user, name="in_use_dataset") + task = TaskFactory(input_data=in_use_dataset) + phase = PhaseFactory() + phase.tasks.add(task) + competition = CompetitionFactory(created_by=self.user) + competition.phases.set([phase]) + + url = reverse("data-detail", args=[in_use_dataset.pk]) + resp = self.client.delete(url) + + self.assertEqual(resp.status_code, 400) + self.assertEqual(resp.data["error"], "Cannot delete dataset: dataset is in use") + self.assertTrue(Data.objects.filter(pk=in_use_dataset.pk).exists()) + + def test_bulk_delete_success(self): + """Multiple datasets deleted successfully.""" + ids = [self.dataset1.pk, self.dataset2.pk] + resp = self.client.post(reverse("data-delete-many"), ids, format="json") + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.data["detail"], "Datasets deleted successfully") + self.assertFalse(Data.objects.filter(pk__in=ids).exists()) + + def test_bulk_delete_with_errors(self): + """Bulk delete should fail entirely if one dataset is not deletable.""" + # include one dataset from another user + ids = [self.dataset1.pk, self.other_dataset.pk] + resp = self.client.post(reverse("data-delete-many"), ids, format="json") + + # Since one dataset is not deletable, expect a 400 response + self.assertEqual(resp.status_code, 400) + self.assertIn("other_dataset", resp.data) + self.assertEqual(resp.data["other_dataset"], "Cannot delete a dataset that is not yours") + + # None should be deleted since the operation failed + self.assertTrue(Data.objects.filter(pk=self.dataset1.pk).exists()) + self.assertTrue(Data.objects.filter(pk=self.other_dataset.pk).exists()) + + def test_cannot_delete_dataset_associated_with_a_submission_in_competition(self): + """If a dataset is a submission linked to a competition phase, it cannot be deleted.""" + # Setup a submission dataset + phase = PhaseFactory() + competition = CompetitionFactory(created_by=self.user) + competition.phases.set([phase]) + submission_dataset = DataFactory(type=Data.SUBMISSION, created_by=self.user, name="submission_dataset") + SubmissionFactory(owner=self.user, phase=phase, data=submission_dataset) + + url = reverse("data-detail", args=[submission_dataset.pk]) + resp = self.client.delete(url) + + self.assertEqual(resp.status_code, 400) + self.assertEqual( + resp.data["error"], + "Cannot delete submission: submission belongs to an existing competition. Please visit the competition and delete your submission from there." + ) + self.assertTrue(Data.objects.filter(pk=submission_dataset.pk).exists()) diff --git a/src/apps/api/views/datasets.py b/src/apps/api/views/datasets.py index ae17dda07..0c95a8a3c 100644 --- a/src/apps/api/views/datasets.py +++ b/src/apps/api/views/datasets.py @@ -144,17 +144,13 @@ def create(self, request, *args, **kwargs): return Response(context, status=status.HTTP_201_CREATED, headers=headers) def destroy(self, request, *args, **kwargs): - # TODO: Confirm this has a test - instance = self.get_object() - - error = self.check_delete_permissions(request, instance) - + dataset = self.get_object() + error = self.check_delete_permissions(request, dataset) if error: return Response( {'error': error}, status=status.HTTP_400_BAD_REQUEST ) - return super().destroy(request, *args, **kwargs) @action(detail=False, methods=('POST',)) From a1699fa64cb9d46e0767390d267e33383c318b1b Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Wed, 12 Nov 2025 03:48:46 +0500 Subject: [PATCH 19/31] added CONTACT_EMAIL to env, and used it in the code where needed rabsed with develop --- .env_sample | 2 ++ src/apps/api/views/competitions.py | 4 ++-- src/apps/pages/views.py | 2 ++ src/apps/profiles/views.py | 2 +- src/settings/base.py | 1 + src/templates/pages/home.html | 2 +- 6 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.env_sample b/.env_sample index 8902c5223..8d8629a3c 100644 --- a/.env_sample +++ b/.env_sample @@ -45,6 +45,8 @@ SELENIUM_HOSTNAME=selenium #DEFAULT_FROM_EMAIL="Codabench " #SERVER_EMAIL=noreply@example.com +# Contact Email +CONTACT_EMAIL=info@codabench.org # ----------------------------------------------------------------------------- # Storage diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index 493a13ea7..4113eb754 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -739,12 +739,12 @@ def rerun_submissions(self, request, pk): # Codabemch public queue if comp.queue is None: can_re_run_submissions = False - error_message = f"You cannot rerun more than {settings.RERUN_SUBMISSION_LIMIT} submissions on Codabench public queue! Contact us on `info@codalab.org` to request a rerun." + error_message = f"You cannot rerun more than {settings.RERUN_SUBMISSION_LIMIT} submissions on Codabench public queue! Contact us on `{settings.CONTACT_EMAIL}` to request a rerun." # Other queue where user is not owner and not organizer elif request.user != comp.queue.owner and request.user not in comp.queue.organizers.all(): can_re_run_submissions = False - error_message = f"You cannot rerun more than {settings.RERUN_SUBMISSION_LIMIT} submissions on a queue which is not yours! Contact us on `info@codalab.org` to request a rerun." + error_message = f"You cannot rerun more than {settings.RERUN_SUBMISSION_LIMIT} submissions on a queue which is not yours! Contact us on `{settings.CONTACT_EMAIL}` to request a rerun." # User can rerun submissions where he is owner or organizer else: diff --git a/src/apps/pages/views.py b/src/apps/pages/views.py index 162d47fdf..38df37527 100644 --- a/src/apps/pages/views.py +++ b/src/apps/pages/views.py @@ -5,6 +5,7 @@ from competitions.models import Submission from announcements.models import Announcement, NewsPost +from django.conf import settings from django.shortcuts import render from utils.data import pretty_bytes @@ -20,6 +21,7 @@ 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/apps/profiles/views.py b/src/apps/profiles/views.py index c4bf6e6e7..333a96e15 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -347,7 +347,7 @@ class CustomPasswordResetView(auth_views.PasswordResetView): We have to use app:view_name syntax in templates like " {% url 'accounts:password_reset_confirm'%} " Therefore we need to tell this view to find the right success_url with that syntax or django won't be able to find the view. - 3. from_email: We want to set the from_email to info@codalab.org - may eventually put in .env file. + 3. from_email: We want to use SERVER_EMAIL already set in the .env # The other commented sections are the defaults for other attributes in auth_views.PasswordResetView. They are in here in case someone wants to customize in the future. All attributes show up in the order shown in the docs. diff --git a/src/settings/base.py b/src/settings/base.py index 7f6b0e79a..b1c1dc15e 100644 --- a/src/settings/base.py +++ b/src/settings/base.py @@ -452,6 +452,7 @@ def setup_celery_logging(**kwargs): DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'Codabench ') SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'noreply@codabench.org') +CONTACT_EMAIL = os.environ.get('CONTACT_EMAIL', 'info@codabench.org') # Django-Su (User impersonation) SU_LOGIN_CALLBACK = 'profiles.admin.su_login_callback' diff --git a/src/templates/pages/home.html b/src/templates/pages/home.html index 485524a41..c323272fa 100644 --- a/src/templates/pages/home.html +++ b/src/templates/pages/home.html @@ -81,7 +81,7 @@

Contribute

-

Interested in joining the development team? Join us on Github or contact us directly.

+

Interested in joining the development team? Join us on Github or contact us directly.

From 9924e0913b000e86739e4ddf9fec0516eab3a4f1 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Wed, 10 Dec 2025 10:34:25 +0100 Subject: [PATCH 20/31] change old wiki link to docs.codabench.org links --- .github/CONTRIBUTING.md | 15 +++++------ README.md | 4 +-- .../Getting-started-with-Codabench.md | 4 +-- .../editor/_competition_details.tag | 4 +-- src/static/riot/tasks/management.tag | 26 +++++++++---------- src/templates/404.html | 2 +- src/templates/base.html | 2 +- src/templates/pages/home.html | 12 ++++----- 8 files changed, 34 insertions(+), 35 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 31aa14768..171579ea6 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,21 +3,20 @@ ## 1. Being a Codabench user. - Create a user account on https://codalab.lisn.fr and on https://codabench.org. -- Register on https://codabench.org to this existing competition (IRIS-tuto) https://www.codabench.org/competitions/1115/ and make a submission (from https://github.com/codalab/competition-examples/tree/master/codabench/iris): sample_result_submission and sample_code_submission. See https://github.com/codalab/codabench/wiki/User_Participating-in-a-Competition -- Create your own private competition (from https://github.com/codalab/competition-examples/tree/master/codabench/ ). See https://github.com/codalab/codabench/wiki/Getting-started-with-Codabench +- Register on https://codabench.org to this existing competition (IRIS-tuto) https://www.codabench.org/competitions/1115/ and make a submission (from https://github.com/codalab/competition-examples/tree/master/codabench/iris): sample_result_submission and sample_code_submission. See http://docs.codabench.org/latest/Participants/User_Participating-in-a-Competition/ +- Create your own private competition (from https://github.com/codalab/competition-examples/tree/master/codabench/ ). See http://docs.codabench.org/latest/Organizers/Benchmark_Creation/Getting-started-with-Codabench/ ## 2. Setting a local instance of Codabench. -- Follow the tutorial in codabench wiki: https://github.com/codalab/codabench/wiki/Codabench-Installation. According to your hosting OS, you might have to tune your environment file a bit. Try without enabling the SSL protocol (doing so, you don't need a domain name for the server). Try using the embedded Minio storage solution instead of a private cloud storage. -- If needed, you can also look into https://github.com/codalab/codabench/wiki/How-to-deploy-Codabench-on-your-server +- Follow the tutorial in codabench wiki: http://docs.codabench.org/latest/Developers_and_Administrators/Codabench-Installation/. According to your hosting OS, you might have to tune your environment file a bit. Try without enabling the SSL protocol (doing so, you don't need a domain name for the server). Try using the embedded Minio storage solution instead of a private cloud storage. +- If needed, you can also look into http://docs.codabench.org/latest/Developers_and_Administrators/How-to-deploy-Codabench-on-your-server/ ## 3. Using one's local instance - Create your own competition and play with it. You can look at the output logs of each different docker container. -- Setting you as an admin of your platform (https://github.com/codalab/codabench/wiki/Administrator-procedures#give-superuser-privileges-to-an-user) and visit the Django Admin menu: https://github.com/codalab/codabench/wiki/Administrator-procedures#give-superuser-privileges-to-an-user - +- Setting you as an admin of your platform (http://docs.codabench.org/latest/Developers_and_Administrators/Administrator-procedures/#give-superuser-privileges-to-a-user) and visit the Django Admin menu: http://docs.codabench.org/latest/Developers_and_Administrators/Administrator-procedures/#give-superuser-privileges-to-a-user ## 4. Setting an autonomous computer-worker on your PC -- Configure and launch the docker container: https://github.com/codalab/codabench/wiki/Compute-Worker-Management---Setup -- Create a private queue on your new own competition on the production server codabench.org: https://github.com/codalab/codabench/wiki/Queue-Management#create-queue +- Configure and launch the docker container: http://docs.codabench.org/latest/Organizers/Running_a_benchmark/Compute-Worker-Management---Setup/ +- Create a private queue on your new own competition on the production server codabench.org: http://docs.codabench.org/latest/Organizers/Running_a_benchmark/Queue-Management/#create-queue - Assign your own compute-worker to this private queue instead of the default queue. diff --git a/README.md b/README.md index c35898589..cda83495b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ To see Codabench in action, visit [codabench.org](https://www.codabench.org/). ## Documentation -- [Codabench Wiki](https://github.com/codalab/codabench/wiki) +- [Codabench Wiki](https://docs.codabench.org) ## Quick installation (for Linux) @@ -30,7 +30,7 @@ You can now login as username "admin" with password "admin" at http://localhost/ If you ever need to reset the database, use the script `./reset_db.sh` -For more information about installation, checkout [Codabench Basic Installation Guide](https://github.com/codalab/codabench/wiki/Codabench-Installation) and [How to Deploy Server](https://github.com/codalab/codabench/wiki/How-to-deploy-Codabench-on-your-server). +For more information about installation, checkout [Codabench Basic Installation Guide](http://docs.codabench.org/latest/Developers_and_Administrators/Codabench-Installation/) and [How to Deploy Server](http://docs.codabench.org/latest/Developers_and_Administrators/How-to-deploy-Codabench-on-your-server/). ## License diff --git a/documentation/docs/Organizers/Benchmark_Creation/Getting-started-with-Codabench.md b/documentation/docs/Organizers/Benchmark_Creation/Getting-started-with-Codabench.md index d30d2ad34..a0025ca21 100644 --- a/documentation/docs/Organizers/Benchmark_Creation/Getting-started-with-Codabench.md +++ b/documentation/docs/Organizers/Benchmark_Creation/Getting-started-with-Codabench.md @@ -1,6 +1,6 @@ [Codabench](https://codabench.org) is an upgraded version of the [CodaLab Competitions](https://codalab.lisn.fr/) platform, allowing you to create either **competitions** or **benchmarks**. A benchmark is essentially an **ever-lasting competition** with **multiple tasks**, for which a participant can make **multiple entries** in the result table. -This getting started tutorial shows a **simple example** of how to create a competition. Advanced users should check [fancier examples](https://github.com/codalab/competition-examples/tree/master/codabench) and [the full documentation](https://github.com/codalab/codabench/wiki). If you simply wish to participate in a benchmark or competition, go to [Participating in a benchmark](../../Participants/User_Participating-in-a-Competition.md). +This getting started tutorial shows a **simple example** of how to create a competition. Advanced users should check [fancier examples](https://github.com/codalab/competition-examples/tree/master/codabench) and [the full documentation](https://docs.codabench.org). If you simply wish to participate in a benchmark or competition, go to [Participating in a benchmark](../../Participants/User_Participating-in-a-Competition.md). ## Getting ready @@ -34,4 +34,4 @@ This getting started tutorial shows a **simple example** of how to create a comp You are done with this simple tutorial. Next, check the more [advanced tutorial](Advanced-Tutorial.md). -You can also check out this blog post: [How to create your first benchmark on Codabench](https://medium.com/@adrienpavao/how-to-create-your-first-benchmark-on-codabench-910e2aee130c). \ No newline at end of file +You can also check out this blog post: [How to create your first benchmark on Codabench](https://medium.com/@adrienpavao/how-to-create-your-first-benchmark-on-codabench-910e2aee130c). diff --git a/src/static/riot/competitions/editor/_competition_details.tag b/src/static/riot/competitions/editor/_competition_details.tag index 4cd12eeb6..345a9d79e 100644 --- a/src/static/riot/competitions/editor/_competition_details.tag +++ b/src/static/riot/competitions/editor/_competition_details.tag @@ -137,7 +137,7 @@