From 4b7c50592d3ec8e0cbe83d41b5f58066183d8156 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Fri, 19 Dec 2025 13:28:59 +0100 Subject: [PATCH 1/9] update django admin interface to make it easier to manage spam --- src/apps/announcements/admin.py | 2 +- src/apps/competitions/admin.py | 5 ++-- src/apps/datasets/admin.py | 20 +++++++++++++-- src/apps/forums/admin.py | 43 ++++++++++++++++++++++++++++++--- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/apps/announcements/admin.py b/src/apps/announcements/admin.py index e675dd528..432ebf1f7 100644 --- a/src/apps/announcements/admin.py +++ b/src/apps/announcements/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin - from . import models + admin.site.register(models.Announcement) admin.site.register(models.NewsPost) diff --git a/src/apps/competitions/admin.py b/src/apps/competitions/admin.py index 942133b4b..7828ff8de 100644 --- a/src/apps/competitions/admin.py +++ b/src/apps/competitions/admin.py @@ -21,7 +21,7 @@ def lookups(self, request, model_admin): in the right sidebar. """ return [ - ("privateSmall", _("Submissions >= 25 and Participants >= 10")), + ("privateSmall", _("Submissions >= 10 and Participants >= 5")), ] def queryset(self, request, queryset): @@ -30,7 +30,7 @@ def queryset(self, request, queryset): provided in the query string and retrievable via `self.value()`. """ - # Only show private competitions with >= 25 submissions and >=10 participants + # Only show private competitions with >= 10 submissions and >=5 participants if self.value() == "privateSmall": return queryset.filter( published=False, @@ -42,6 +42,7 @@ def queryset(self, request, queryset): class CompetitionAdmin(admin.ModelAdmin): search_fields = ['title', 'docker_image', 'created_by__username'] list_display = ['id', 'title', 'created_by', 'is_featured'] + list_display_links = ['id', 'title'] list_filter = ['is_featured', privateCompetitionsFilter] diff --git a/src/apps/datasets/admin.py b/src/apps/datasets/admin.py index 2fc94340d..bae1c86fb 100644 --- a/src/apps/datasets/admin.py +++ b/src/apps/datasets/admin.py @@ -1,6 +1,22 @@ from django.contrib import admin - +from profiles.models import User from . import models -admin.site.register(models.Data) +@admin.action(description="Deactivate Account and Delete Item") +def DeactivateAccount(modeladmin, request, queryset): + for obj in queryset: + user = User.objects.get(id=obj.created_by_id) + user.is_banned = True + user.save() + queryset.delete() + + +class DataExpansion(admin.ModelAdmin): + list_display = ["name", "description", "created_by", "type", "is_public", "is_verified", "file_size"] + search_fields = ["created_by__username", "name", "type", "description", "file_name", "file_size"] + list_filter = ["is_public", "is_verified"] + actions = [DeactivateAccount] + + +admin.site.register(models.Data, DataExpansion) diff --git a/src/apps/forums/admin.py b/src/apps/forums/admin.py index c73a6e1b5..8dc6b3e07 100644 --- a/src/apps/forums/admin.py +++ b/src/apps/forums/admin.py @@ -1,8 +1,43 @@ from django.contrib import admin - +from profiles.models import User from . import models -admin.site.register(models.Forum) -admin.site.register(models.Thread) -admin.site.register(models.Post) +@admin.action(description="Deactivate Account and Delete Item") +def DeactivateAccountThread(modeladmin, request, queryset): + for obj in queryset: + user = User.objects.get(id=obj.started_by_id) + user.is_banned = True + user.save() + queryset.delete() + + +@admin.action(description="Deactivate Account and Delete Item") +def DeactivateAccountPost(modeladmin, request, queryset): + for obj in queryset: + user = User.objects.get(id=obj.posted_by_id) + user.is_banned = True + user.save() + queryset.delete() + + +class ForumsExpansion(admin.ModelAdmin): + list_display = ["competition"] + search_fields = ["competition"] + + +class ThreadExpansion(admin.ModelAdmin): + list_display = ["title", "started_by"] + search_fields = ["title", "started_by__username"] + actions = [DeactivateAccountThread] + + +class PostExpansion(admin.ModelAdmin): + list_display = ["content", "posted_by"] + search_fields = [] + actions = [DeactivateAccountPost] + + +admin.site.register(models.Forum, ForumsExpansion) +admin.site.register(models.Thread, ThreadExpansion) +admin.site.register(models.Post, PostExpansion) From d919701c4918d52e0d810d5e44a91a9ceb432b75 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Fri, 19 Dec 2025 18:54:07 +0100 Subject: [PATCH 2/9] add option to export email and username of organizers as a CSV file or JSON --- src/apps/competitions/admin.py | 45 ++++++++++++++++++++++++++++------ src/apps/datasets/admin.py | 19 ++++++++++++-- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/apps/competitions/admin.py b/src/apps/competitions/admin.py index 7828ff8de..28a08cd2b 100644 --- a/src/apps/competitions/admin.py +++ b/src/apps/competitions/admin.py @@ -1,6 +1,9 @@ from django.contrib import admin from django.utils.translation import gettext_lazy as _ - +import json +import csv +from django.http import HttpResponse +from profiles.models import User from . import models @@ -33,17 +36,43 @@ def queryset(self, request, queryset): # Only show private competitions with >= 10 submissions and >=5 participants if self.value() == "privateSmall": return queryset.filter( - published=False, - submissions_count__gte=10, - participants_count__gte=5 + published=False, submissions_count__gte=10, participants_count__gte=5 ) +# This will export the email of all the selected competition creators, removing duplications and banned users +def export_as_csv(modeladmin, request, queryset): + response = HttpResponse( + content_type="text/csv", + headers={"Content-Disposition": 'attachment; filename="email_username_list.csv"'}, + ) + writer = csv.writer(response) + writer.writerow(["Username", "Email", "Competition title", "Participants Count", "Submissions Count"]) + email_list = {} + for obj in queryset: + user = User.objects.get(id=obj.created_by_id) + if not user.is_banned and user.email not in email_list.values(): + email_list.update({user.username: user.email}) + writer.writerow([user.username, user.email, obj.title, obj.participants_count, obj.submissions_count]) + return response + + +# This will export the email of all the selected competition creators, removing duplications and banned users +def export_as_json(modeladmin, request, queryset): + email_list = {} + for obj in queryset: + user = User.objects.get(id=obj.created_by_id) + if not user.is_banned and user.email not in email_list.values(): + email_list.update({user.username: user.email}) + return HttpResponse(json.dumps(email_list), content_type="application/json") + + class CompetitionAdmin(admin.ModelAdmin): - search_fields = ['title', 'docker_image', 'created_by__username'] - list_display = ['id', 'title', 'created_by', 'is_featured'] - list_display_links = ['id', 'title'] - list_filter = ['is_featured', privateCompetitionsFilter] + search_fields = ["title", "docker_image", "created_by__username"] + list_display = ["id", "title", "created_by", "is_featured"] + list_display_links = ["id", "title"] + actions = [export_as_json, export_as_csv] + list_filter = ["is_featured", privateCompetitionsFilter] admin.site.register(models.Competition, CompetitionAdmin) diff --git a/src/apps/datasets/admin.py b/src/apps/datasets/admin.py index bae1c86fb..f78380880 100644 --- a/src/apps/datasets/admin.py +++ b/src/apps/datasets/admin.py @@ -13,8 +13,23 @@ def DeactivateAccount(modeladmin, request, queryset): class DataExpansion(admin.ModelAdmin): - list_display = ["name", "description", "created_by", "type", "is_public", "is_verified", "file_size"] - search_fields = ["created_by__username", "name", "type", "description", "file_name", "file_size"] + list_display = [ + "name", + "description", + "created_by", + "type", + "is_public", + "is_verified", + "file_size", + ] + search_fields = [ + "created_by__username", + "name", + "type", + "description", + "file_name", + "file_size", + ] list_filter = ["is_public", "is_verified"] actions = [DeactivateAccount] From 3bc7cc3d09dd0b9a9d1ca7dd020efd1478795283 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Fri, 19 Dec 2025 19:09:59 +0100 Subject: [PATCH 3/9] add option to export email and username of queue owners as a CSV file or JSON --- src/apps/competitions/admin.py | 4 ++-- src/apps/profiles/admin.py | 8 +++---- src/apps/queues/admin.py | 40 +++++++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/apps/competitions/admin.py b/src/apps/competitions/admin.py index 28a08cd2b..f0520f1ff 100644 --- a/src/apps/competitions/admin.py +++ b/src/apps/competitions/admin.py @@ -67,7 +67,7 @@ def export_as_json(modeladmin, request, queryset): return HttpResponse(json.dumps(email_list), content_type="application/json") -class CompetitionAdmin(admin.ModelAdmin): +class CompetitionExpansion(admin.ModelAdmin): search_fields = ["title", "docker_image", "created_by__username"] list_display = ["id", "title", "created_by", "is_featured"] list_display_links = ["id", "title"] @@ -75,7 +75,7 @@ class CompetitionAdmin(admin.ModelAdmin): list_filter = ["is_featured", privateCompetitionsFilter] -admin.site.register(models.Competition, CompetitionAdmin) +admin.site.register(models.Competition, CompetitionExpansion) admin.site.register(models.CompetitionCreationTaskStatus) admin.site.register(models.CompetitionParticipant) admin.site.register(models.Page) diff --git a/src/apps/profiles/admin.py b/src/apps/profiles/admin.py index eafc21cdf..27a70e1db 100644 --- a/src/apps/profiles/admin.py +++ b/src/apps/profiles/admin.py @@ -3,7 +3,7 @@ from .models import User, DeletedUser, Organization, Membership -class UserAdmin(admin.ModelAdmin): +class UserExpansion(admin.ModelAdmin): # The following two lines are needed for Django-su: change_form_template = "admin/auth/user/change_form.html" change_list_template = "admin/auth/user/change_list.html" @@ -12,14 +12,14 @@ class UserAdmin(admin.ModelAdmin): list_display = ['username', 'email', 'is_staff', 'is_superuser', 'is_banned'] -class DeletedUserAdmin(admin.ModelAdmin): +class DeletedUserExpansion(admin.ModelAdmin): list_display = ('user_id', 'username', 'email', 'deleted_at') search_fields = ('username', 'email') list_filter = ('deleted_at',) -admin.site.register(User, UserAdmin) -admin.site.register(DeletedUser, DeletedUserAdmin) +admin.site.register(User, UserExpansion) +admin.site.register(DeletedUser, DeletedUserExpansion) admin.site.register(Organization) admin.site.register(Membership) diff --git a/src/apps/queues/admin.py b/src/apps/queues/admin.py index 160e2b8b3..2e6ffe339 100644 --- a/src/apps/queues/admin.py +++ b/src/apps/queues/admin.py @@ -1,5 +1,43 @@ from django.contrib import admin from queues import models +import json +import csv +from django.http import HttpResponse +from profiles.models import User -admin.site.register(models.Queue) +# This will export the email of all the selected competition creators, removing duplications and banned users +def export_as_csv(modeladmin, request, queryset): + response = HttpResponse( + content_type="text/csv", + headers={"Content-Disposition": 'attachment; filename="email_username_list.csv"'}, + ) + writer = csv.writer(response) + writer.writerow(["Username", "Email", "Queue_Name"]) + email_list = {} + for obj in queryset: + user = User.objects.get(id=obj.owner_id) + if not user.is_banned and user.email not in email_list.values(): + email_list.update({user.username: user.email}) + writer.writerow([user.username, user.email, obj.name]) + return response + + +# This will export the email of all the selected competition creators, removing duplications and banned users +def export_as_json(modeladmin, request, queryset): + email_list = {} + for obj in queryset: + user = User.objects.get(id=obj.owner_id) + if not user.is_banned and user.email not in email_list.values(): + email_list.update({user.username: user.email}) + return HttpResponse(json.dumps(email_list), content_type="application/json") + + +class QueueExpansion(admin.ModelAdmin): + list_display = ["name", "owner", "is_public"] + list_filter = ["is_public"] + search_fields = ["name", "owner__username"] + actions = [export_as_csv, export_as_json] + + +admin.site.register(models.Queue, QueueExpansion) From c155a04be274e0a88890ab376b213bdfb5ddeac5 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Tue, 23 Dec 2025 12:04:49 +0100 Subject: [PATCH 4/9] use raw_id_fields to make django admin pages load much faster --- src/apps/competitions/admin.py | 90 +++++++++++++++++++++++++++++++--- src/apps/datasets/admin.py | 3 ++ src/apps/forums/admin.py | 11 +++-- src/apps/leaderboards/admin.py | 25 ++++++++-- src/apps/profiles/admin.py | 29 ++++++++--- src/apps/queues/admin.py | 9 ++-- src/apps/tasks/admin.py | 39 ++++++++++++++- 7 files changed, 178 insertions(+), 28 deletions(-) diff --git a/src/apps/competitions/admin.py b/src/apps/competitions/admin.py index f0520f1ff..2dc5beda8 100644 --- a/src/apps/competitions/admin.py +++ b/src/apps/competitions/admin.py @@ -44,16 +44,34 @@ def queryset(self, request, queryset): def export_as_csv(modeladmin, request, queryset): response = HttpResponse( content_type="text/csv", - headers={"Content-Disposition": 'attachment; filename="email_username_list.csv"'}, + headers={ + "Content-Disposition": 'attachment; filename="email_username_list.csv"' + }, ) writer = csv.writer(response) - writer.writerow(["Username", "Email", "Competition title", "Participants Count", "Submissions Count"]) + writer.writerow( + [ + "Username", + "Email", + "Competition title", + "Participants Count", + "Submissions Count", + ] + ) email_list = {} for obj in queryset: user = User.objects.get(id=obj.created_by_id) if not user.is_banned and user.email not in email_list.values(): email_list.update({user.username: user.email}) - writer.writerow([user.username, user.email, obj.title, obj.participants_count, obj.submissions_count]) + writer.writerow( + [ + user.username, + user.email, + obj.title, + obj.participants_count, + obj.submissions_count, + ] + ) return response @@ -72,12 +90,68 @@ class CompetitionExpansion(admin.ModelAdmin): list_display = ["id", "title", "created_by", "is_featured"] list_display_links = ["id", "title"] actions = [export_as_json, export_as_csv] + raw_id_fields = ["created_by", "collaborators"] list_filter = ["is_featured", privateCompetitionsFilter] +class SubmissionExpansion(admin.ModelAdmin): + # Raw Id Fields changes the field from displaying everything in a drop down menu into an id fields, which makes the page loads much faster (removes huge SELECT from the database) + raw_id_fields = [ + "organization", + "owner", + "phase", + "data", + "task", + "leaderboard", + "participant", + "queue", + "created_by_migration", + "parent", + "scores", + ] + search_fields = ["owner__username"] + list_display = [ + "id", + "owner", + "task", + "is_public", + "has_children", + "is_soft_deleted", + ] + list_filter = ["is_public", "has_children", "is_soft_deleted"] + + +class CompetitionCreationTaskStatusExpansion(admin.ModelAdmin): + raw_id_fields = ["dataset", "created_by", "resulting_competition"] + list_display = ["id", "created_by", "resulting_competition", "status"] + search_fields = ["id", "created_by__username"] + list_filter = ["status"] + + +class CompetitionParticipantExpansion(admin.ModelAdmin): + raw_id_fields = ["user", "competition"] + list_display = ["id", "user", "competition", "status"] + list_filter = ["status"] + search_fields = ["user__username", "competition"] + + +class PageExpansion(admin.ModelAdmin): + raw_id_fields = ["competition"] + list_display = ["id", "competition"] + search_fields = ["competition", "content"] + + +class PhaseExpansion(admin.ModelAdmin): + raw_id_fields = ["competition", "leaderboard", "public_data", "starting_kit"] + list_display = ["id", "competition", "name"] + search_fields = ["id", "competition", "name"] + + admin.site.register(models.Competition, CompetitionExpansion) -admin.site.register(models.CompetitionCreationTaskStatus) -admin.site.register(models.CompetitionParticipant) -admin.site.register(models.Page) -admin.site.register(models.Phase) -admin.site.register(models.Submission) +admin.site.register( + models.CompetitionCreationTaskStatus, CompetitionCreationTaskStatusExpansion +) +admin.site.register(models.CompetitionParticipant, CompetitionParticipantExpansion) +admin.site.register(models.Page, PageExpansion) +admin.site.register(models.Phase, PhaseExpansion) +admin.site.register(models.Submission, SubmissionExpansion) diff --git a/src/apps/datasets/admin.py b/src/apps/datasets/admin.py index f78380880..fde4ee21a 100644 --- a/src/apps/datasets/admin.py +++ b/src/apps/datasets/admin.py @@ -13,7 +13,9 @@ def DeactivateAccount(modeladmin, request, queryset): class DataExpansion(admin.ModelAdmin): + raw_id_fields = ["created_by", "competition"] list_display = [ + "id", "name", "description", "created_by", @@ -23,6 +25,7 @@ class DataExpansion(admin.ModelAdmin): "file_size", ] search_fields = [ + "id", "created_by__username", "name", "type", diff --git a/src/apps/forums/admin.py b/src/apps/forums/admin.py index 8dc6b3e07..1059fccb0 100644 --- a/src/apps/forums/admin.py +++ b/src/apps/forums/admin.py @@ -22,19 +22,22 @@ def DeactivateAccountPost(modeladmin, request, queryset): class ForumsExpansion(admin.ModelAdmin): - list_display = ["competition"] + raw_id_fields = ["competition"] + list_display = ["id", "competition"] search_fields = ["competition"] class ThreadExpansion(admin.ModelAdmin): - list_display = ["title", "started_by"] + raw_id_fields = ["forum", "started_by"] + list_display = ["id", "title", "started_by"] search_fields = ["title", "started_by__username"] actions = [DeactivateAccountThread] class PostExpansion(admin.ModelAdmin): - list_display = ["content", "posted_by"] - search_fields = [] + raw_id_fields = ["thread", "posted_by"] + list_display = ["id", "content", "posted_by"] + search_fields = ["content", "posted_by__username"] actions = [DeactivateAccountPost] diff --git a/src/apps/leaderboards/admin.py b/src/apps/leaderboards/admin.py index f3cac71ce..db86c577d 100644 --- a/src/apps/leaderboards/admin.py +++ b/src/apps/leaderboards/admin.py @@ -3,6 +3,25 @@ from . import models -admin.site.register(models.Leaderboard) -admin.site.register(models.Column) -admin.site.register(models.SubmissionScore) +class LeaderboardExpansion(admin.ModelAdmin): + list_display = ["id", "title", "submission_rule", "hidden"] + search_fields = ["id", "title"] + list_filter = ["hidden"] + + +class ColumExpansion(admin.ModelAdmin): + raw_id_fields = ["leaderboard"] + list_display = ["id", "title", "hidden"] + search_fields = ["id", "title"] + list_filter = ["hidden"] + + +class SubmissionScoreExpansion(admin.ModelAdmin): + raw_id_fields = ["column"] + list_display = ["id", "column", "score"] + search_fields = ["id", "column"] + + +admin.site.register(models.Leaderboard, LeaderboardExpansion) +admin.site.register(models.Column, ColumExpansion) +admin.site.register(models.SubmissionScore, SubmissionScoreExpansion) diff --git a/src/apps/profiles/admin.py b/src/apps/profiles/admin.py index 27a70e1db..70b8337d4 100644 --- a/src/apps/profiles/admin.py +++ b/src/apps/profiles/admin.py @@ -7,21 +7,34 @@ class UserExpansion(admin.ModelAdmin): # The following two lines are needed for Django-su: 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', 'is_banned'] - list_display = ['username', 'email', 'is_staff', 'is_superuser', 'is_banned'] + search_fields = ["username", "email", "id"] + list_filter = ["is_staff", "is_superuser", "is_deleted", "is_bot", "is_banned"] + list_display = ["id", "username", "email", "is_staff", "is_superuser", "is_banned"] + raw_id_fields = ["oidc_organization", "groups"] class DeletedUserExpansion(admin.ModelAdmin): - list_display = ('user_id', 'username', 'email', 'deleted_at') - search_fields = ('username', 'email') - list_filter = ('deleted_at',) + list_display = ("user_id", "username", "email", "deleted_at") + search_fields = ("username", "email") + list_filter = ("deleted_at",) + + +class MembershipExpansion(admin.ModelAdmin): + raw_id_fields = ["organization", "user"] + list_display = ["id", "organization", "user", "group"] + search_fields = ["id", "user__username", "token"] + + +class OrganizationExpansion(admin.ModelAdmin): + raw_id_fields = ["user_record"] + list_display = ["id", "name", "email", "description"] + search_fields = ["name", "email", "description"] admin.site.register(User, UserExpansion) admin.site.register(DeletedUser, DeletedUserExpansion) -admin.site.register(Organization) -admin.site.register(Membership) +admin.site.register(Organization, OrganizationExpansion) +admin.site.register(Membership, MembershipExpansion) def su_login_callback(user): diff --git a/src/apps/queues/admin.py b/src/apps/queues/admin.py index 2e6ffe339..cc7152801 100644 --- a/src/apps/queues/admin.py +++ b/src/apps/queues/admin.py @@ -10,7 +10,9 @@ def export_as_csv(modeladmin, request, queryset): response = HttpResponse( content_type="text/csv", - headers={"Content-Disposition": 'attachment; filename="email_username_list.csv"'}, + headers={ + "Content-Disposition": 'attachment; filename="email_username_list.csv"' + }, ) writer = csv.writer(response) writer.writerow(["Username", "Email", "Queue_Name"]) @@ -34,9 +36,10 @@ def export_as_json(modeladmin, request, queryset): class QueueExpansion(admin.ModelAdmin): - list_display = ["name", "owner", "is_public"] + raw_id_fields = ["owner", "organizers"] + list_display = ["id", "name", "owner", "is_public"] list_filter = ["is_public"] - search_fields = ["name", "owner__username"] + search_fields = ["name", "owner__username", "organizers__username"] actions = [export_as_csv, export_as_json] diff --git a/src/apps/tasks/admin.py b/src/apps/tasks/admin.py index 4ff0a7c17..b91200eef 100644 --- a/src/apps/tasks/admin.py +++ b/src/apps/tasks/admin.py @@ -2,5 +2,40 @@ from . import models -admin.site.register(models.Task) -admin.site.register(models.Solution) + +class TaskExpansion(admin.ModelAdmin): + raw_id_fields = [ + "created_by", + "shared_with", + "ingestion_program", + "input_data", + "reference_data", + "scoring_program", + ] + list_display = [ + "id", + "created_by", + "name", + "description", + "ingestion_only_during_scoring", + "is_public", + ] + list_filter = ["is_public", "ingestion_only_during_scoring"] + search_fields = [ + "id", + "name", + "created_by__username", + ] + + +class SolutionExpansion(admin.ModelAdmin): + raw_id_fields = ["tasks", "data"] + list_display = ["id", "name", "description", "data", "is_public"] + list_filter = ["is_public"] + search_fields = [ + "id", + ] + + +admin.site.register(models.Task, TaskExpansion) +admin.site.register(models.Solution, SolutionExpansion) From 1bafcbdb46455a3f05da877b5a969e595dc21e78 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Tue, 23 Dec 2025 14:57:48 +0100 Subject: [PATCH 5/9] make file size human readable --- src/apps/datasets/admin.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/apps/datasets/admin.py b/src/apps/datasets/admin.py index fde4ee21a..b0ec92849 100644 --- a/src/apps/datasets/admin.py +++ b/src/apps/datasets/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from profiles.models import User from . import models +from django.template.defaultfilters import filesizeformat @admin.action(description="Deactivate Account and Delete Item") @@ -22,7 +23,7 @@ class DataExpansion(admin.ModelAdmin): "type", "is_public", "is_verified", - "file_size", + "filesize_human", ] search_fields = [ "id", @@ -34,6 +35,14 @@ class DataExpansion(admin.ModelAdmin): "file_size", ] list_filter = ["is_public", "is_verified"] + + # Convert the file size from bytes to KB,MB,GB etc to make it more readable in the list_display + @admin.display(description="File size", ordering="file_size") + def filesize_human(self, obj): + if not obj.file_size: + return "-" + return filesizeformat(obj.file_size) + actions = [DeactivateAccount] From b16843f8a445a4a233ed2a7c4bb8e2bfbd87ce35 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Wed, 24 Dec 2025 12:30:29 +0100 Subject: [PATCH 6/9] re-arranged the fieldsets of some admin pages to be easier to navigate --- src/apps/competitions/admin.py | 135 ++++++++++++++++++++++++++++++++- src/apps/profiles/admin.py | 78 +++++++++++++++++++ 2 files changed, 212 insertions(+), 1 deletion(-) diff --git a/src/apps/competitions/admin.py b/src/apps/competitions/admin.py index 2dc5beda8..9f2cb5ea7 100644 --- a/src/apps/competitions/admin.py +++ b/src/apps/competitions/admin.py @@ -90,8 +90,50 @@ class CompetitionExpansion(admin.ModelAdmin): list_display = ["id", "title", "created_by", "is_featured"] list_display_links = ["id", "title"] actions = [export_as_json, export_as_csv] - raw_id_fields = ["created_by", "collaborators"] + raw_id_fields = ["created_by", "collaborators", "queue"] list_filter = ["is_featured", privateCompetitionsFilter] + fieldsets = [ + ( + None, + { + "fields": [ + ("title", "docker_image", "competition_type"), + "secret_key", + "terms", + "description", + "fact_sheet", + "contact_email", + "reward", + "report", + "submissions_count", + "participants_count", + "created_when", + ] + }, + ), + ("Raw ID Fields", {"fields": ["created_by", "collaborators", "queue"]}), + ( + "Checkboxes", + { + "fields": [ + "published", + "registration_auto_approve", + "is_migrating", + "enable_detailed_results", + "show_detailed_results_in_submission_panel", + "show_detailed_results_in_leaderboard", + "make_programs_available", + "make_input_data_available", + "allow_robot_submissions", + "auto_run_submissions", + "can_participants_make_submissions_public", + "is_featured", + "forum_enabled", + ] + }, + ), + ("Files", {"fields": ["logo", "logo_icon"]}), + ] class SubmissionExpansion(admin.ModelAdmin): @@ -119,6 +161,64 @@ class SubmissionExpansion(admin.ModelAdmin): "is_soft_deleted", ] list_filter = ["is_public", "has_children", "is_soft_deleted"] + fieldsets = [ + ( + None, + { + "fields": [ + ("status", "ingestion_worker_hostname", "scoring_worker_hostname"), + "status_details", + "description", + "prediction_result_file_size", + "scoring_result_file_size", + "detailed_result_file_size", + "md5", + "secret", + "celery_task_id", + "name", + "fact_sheet_answers", + "created_when", + "started_when", + "soft_deleted_when", + ], + }, + ), + ( + "Raw ID Fields", + { + "fields": [ + "owner", + "organization", + "phase", + "data", + "task", + "leaderboard", + "participant", + "queue", + "created_by_migration", + "scores", + "parent", + ], + }, + ), + ( + "Checkboxes", + { + "fields": [ + "appear_on_leaderboards", + "is_public", + "is_specific_task_re_run", + "is_migrated", + "has_children", + "is_soft_deleted", + ], + }, + ), + ( + "Files", + {"fields": ["prediction_result", "scoring_result", "detailed_result"]}, + ), + ] class CompetitionCreationTaskStatusExpansion(admin.ModelAdmin): @@ -145,6 +245,39 @@ class PhaseExpansion(admin.ModelAdmin): raw_id_fields = ["competition", "leaderboard", "public_data", "starting_kit"] list_display = ["id", "competition", "name"] search_fields = ["id", "competition", "name"] + fieldsets = [ + ( + None, + { + "fields": [ + ("name", "status", "execution_time_limit"), + ("max_submissions_per_day", "max_submissions_per_person"), + "index", + "description", + "start", + "end", + ] + }, + ), + ( + "Raw ID Fields", + {"fields": ["competition", "leaderboard", "public_data", "starting_kit"]}, + ), + ( + "Checkboxes", + { + "fields": [ + "is_final_phase", + "auto_migrate_to_this_phase", + "has_been_migrated", + "hide_output", + "hide_prediction_output", + "hide_score_output", + "has_max_submissions", + ] + }, + ), + ] admin.site.register(models.Competition, CompetitionExpansion) diff --git a/src/apps/profiles/admin.py b/src/apps/profiles/admin.py index 70b8337d4..150180135 100644 --- a/src/apps/profiles/admin.py +++ b/src/apps/profiles/admin.py @@ -10,7 +10,85 @@ class UserExpansion(admin.ModelAdmin): search_fields = ["username", "email", "id"] list_filter = ["is_staff", "is_superuser", "is_deleted", "is_bot", "is_banned"] list_display = ["id", "username", "email", "is_staff", "is_superuser", "is_banned"] + list_display_links = ["id", "username"] raw_id_fields = ["oidc_organization", "groups"] + fieldsets = [ + ( + None, + { + "fields": [ + ("username", "slug", "email"), + "password", + "groups", + "user_permissions", + "date_joined", + "last_login", + "quota", + ] + }, + ), + ( + "Checkboxes", + { + "fields": [ + ("is_active", "is_bot"), + ( + "organizer_direct_message_updates", + "allow_forum_notifications", + "allow_organization_invite_emails", + ), + ("is_superuser", "is_staff"), + "is_deleted", + "is_banned", + ] + }, + ), + ( + "Advanced Options", + { + "classes": ["collapse"], + "fields": [ + "display_name", + "first_name", + "last_name", + "title", + "location", + "biography", + "personal_url", + "linkedin_url", + "twitter_url", + "github_url", + "github_uid", + "avatar_url", + "url", + "html_url", + "name", + "company", + "bio", + "github_info", + ], + }, + ), + ( + "OIDC", + { + "classes": ["collapse"], + "fields": ["is_created_using_oidc", "oidc_organization"], + }, + ), + ( + "RabbitMQ", + { + "classes": ["collapse"], + "fields": [ + "rabbitmq_queue_limit", + "rabbitmq_username", + "rabbitmq_password", + ], + }, + ), + ("Files", {"classes": ["collapse"], "fields": ["photo"]}), + ] class DeletedUserExpansion(admin.ModelAdmin): From 6dff9594c9de9cadd455fd9a0a7624770e125c23 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Wed, 24 Dec 2025 13:00:47 +0100 Subject: [PATCH 7/9] add small description about size (bytes of GB); limit text length displayed in list displays --- src/apps/datasets/admin.py | 11 ++++++++++- .../migrations/0014_alter_data_file_size.py | 18 ++++++++++++++++++ src/apps/datasets/models.py | 2 +- src/apps/forums/admin.py | 11 ++++++++++- src/apps/profiles/admin.py | 2 +- .../migrations/0020_alter_user_quota.py | 18 ++++++++++++++++++ src/apps/profiles/models.py | 2 +- src/apps/queues/admin.py | 1 + 8 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/apps/datasets/migrations/0014_alter_data_file_size.py create mode 100644 src/apps/profiles/migrations/0020_alter_user_quota.py diff --git a/src/apps/datasets/admin.py b/src/apps/datasets/admin.py index b0ec92849..86cb7777c 100644 --- a/src/apps/datasets/admin.py +++ b/src/apps/datasets/admin.py @@ -18,7 +18,7 @@ class DataExpansion(admin.ModelAdmin): list_display = [ "id", "name", - "description", + "description_limited", "created_by", "type", "is_public", @@ -36,6 +36,15 @@ class DataExpansion(admin.ModelAdmin): ] list_filter = ["is_public", "is_verified"] + @admin.display(description="Description", ordering="description") + def description_limited(self, obj): + if not obj.description: + return "-" + if len(obj.description) > 500: + return obj.description[:500] + "(...)" + else: + return obj.description[:500] + # Convert the file size from bytes to KB,MB,GB etc to make it more readable in the list_display @admin.display(description="File size", ordering="file_size") def filesize_human(self, obj): diff --git a/src/apps/datasets/migrations/0014_alter_data_file_size.py b/src/apps/datasets/migrations/0014_alter_data_file_size.py new file mode 100644 index 000000000..2c6d9b96b --- /dev/null +++ b/src/apps/datasets/migrations/0014_alter_data_file_size.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2025-12-24 11:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datasets', '0013_merge_0011_auto_20250623_1341_0012_delete_datagroup'), + ] + + operations = [ + migrations.AlterField( + model_name='data', + name='file_size', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Size in Bytes', max_digits=15, null=True), + ), + ] diff --git a/src/apps/datasets/models.py b/src/apps/datasets/models.py index 668e93977..fd4f7fef8 100644 --- a/src/apps/datasets/models.py +++ b/src/apps/datasets/models.py @@ -57,7 +57,7 @@ class Data(models.Model): key = models.UUIDField(default=uuid.uuid4, blank=True, unique=True) is_public = models.BooleanField(default=False) upload_completed_successfully = models.BooleanField(default=False) - file_size = models.DecimalField(max_digits=15, decimal_places=2, null=True, blank=True) # in Bytes + file_size = models.DecimalField(max_digits=15, decimal_places=2, null=True, blank=True, help_text="Size in Bytes") # in Bytes # This is true if the Data model was created as part of unpacking a competition. Competition bundles themselves # are NOT marked True, since they are not created by unpacking! diff --git a/src/apps/forums/admin.py b/src/apps/forums/admin.py index 1059fccb0..cc49d434e 100644 --- a/src/apps/forums/admin.py +++ b/src/apps/forums/admin.py @@ -36,10 +36,19 @@ class ThreadExpansion(admin.ModelAdmin): class PostExpansion(admin.ModelAdmin): raw_id_fields = ["thread", "posted_by"] - list_display = ["id", "content", "posted_by"] + list_display = ["id", "content_limited", "posted_by"] search_fields = ["content", "posted_by__username"] actions = [DeactivateAccountPost] + @admin.display(description="Content", ordering="content") + def content_limited(self, obj): + if not obj.content: + return "-" + if len(obj.content) > 500: + return obj.content[:500] + "(...)" + else: + return obj.content[:500] + admin.site.register(models.Forum, ForumsExpansion) admin.site.register(models.Thread, ThreadExpansion) diff --git a/src/apps/profiles/admin.py b/src/apps/profiles/admin.py index 150180135..ba6868ef4 100644 --- a/src/apps/profiles/admin.py +++ b/src/apps/profiles/admin.py @@ -44,7 +44,7 @@ class UserExpansion(admin.ModelAdmin): }, ), ( - "Advanced Options", + "Extra Information", { "classes": ["collapse"], "fields": [ diff --git a/src/apps/profiles/migrations/0020_alter_user_quota.py b/src/apps/profiles/migrations/0020_alter_user_quota.py new file mode 100644 index 000000000..5ec46b108 --- /dev/null +++ b/src/apps/profiles/migrations/0020_alter_user_quota.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2025-12-24 11:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0019_merge_0017_user_is_banned_0018_auto_20250623_1719'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='quota', + field=models.BigIntegerField(default=15, help_text='Size in GB'), + ), + ] diff --git a/src/apps/profiles/models.py b/src/apps/profiles/models.py index 95ff20b34..042c7d0a1 100644 --- a/src/apps/profiles/models.py +++ b/src/apps/profiles/models.py @@ -80,7 +80,7 @@ class User(AbstractBaseUser, PermissionsMixin): date_joined = models.DateTimeField(default=now) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) - quota = models.BigIntegerField(default=settings.DEFAULT_USER_QUOTA, null=False) + quota = models.BigIntegerField(default=settings.DEFAULT_USER_QUOTA, null=False, help_text="Size in GB") # Fields for OIDC authentication is_created_using_oidc = models.BooleanField(default=False) diff --git a/src/apps/queues/admin.py b/src/apps/queues/admin.py index cc7152801..ce1029831 100644 --- a/src/apps/queues/admin.py +++ b/src/apps/queues/admin.py @@ -38,6 +38,7 @@ def export_as_json(modeladmin, request, queryset): class QueueExpansion(admin.ModelAdmin): raw_id_fields = ["owner", "organizers"] list_display = ["id", "name", "owner", "is_public"] + list_display_links = ["id", "name"] list_filter = ["is_public"] search_fields = ["name", "owner__username", "organizers__username"] actions = [export_as_csv, export_as_json] From eeec1f62e2829e7eb4081b93e21dab47b3f9729d Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Wed, 24 Dec 2025 20:41:25 +0100 Subject: [PATCH 8/9] add custom text in list filter --- src/apps/announcements/admin.py | 24 ++++- src/apps/competitions/admin.py | 132 +++++++++++++++++++------- src/apps/forums/admin.py | 6 +- src/apps/oidc_configurations/admin.py | 8 +- src/apps/profiles/admin.py | 49 +++++++++- src/apps/queues/admin.py | 2 +- src/templates/admin/input_filter.html | 25 +++++ 7 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 src/templates/admin/input_filter.html diff --git a/src/apps/announcements/admin.py b/src/apps/announcements/admin.py index 432ebf1f7..16fc499db 100644 --- a/src/apps/announcements/admin.py +++ b/src/apps/announcements/admin.py @@ -2,5 +2,25 @@ from . import models -admin.site.register(models.Announcement) -admin.site.register(models.NewsPost) +class NewsPostExpansion(admin.ModelAdmin): + list_display = ["id", "title", "link"] + list_display_links = ["id", "title"] + search_fields = ["id", "title", "link"] + + +class AnnouncementExpansion(admin.ModelAdmin): + list_display = ["id", "text_limited"] + list_display_links = ["id", "text_limited"] + + @admin.display(description="text", ordering="text") + def text_limited(self, obj): + if not obj.text: + return "-" + if len(obj.text) > 500: + return obj.text[:500] + "(...)" + else: + return obj.text[:500] + + +admin.site.register(models.Announcement, AnnouncementExpansion) +admin.site.register(models.NewsPost, NewsPostExpansion) diff --git a/src/apps/competitions/admin.py b/src/apps/competitions/admin.py index 9f2cb5ea7..c7adbe1d4 100644 --- a/src/apps/competitions/admin.py +++ b/src/apps/competitions/admin.py @@ -7,37 +7,46 @@ from . import models -class privateCompetitionsFilter(admin.SimpleListFilter): +# General class used to make custom filter +class InputFilter(admin.SimpleListFilter): + template = "admin/input_filter.html" + + def lookups(self, request, model_admin): + # Dummy, required to show the filter. + return ((),) + + def choices(self, changelist): + # Grab only the "all" option. + all_choice = next(super().choices(changelist)) + all_choice["query_parts"] = ( + (k, v) + for k, v in changelist.get_filters_params().items() + if k != self.parameter_name + ) + yield all_choice + + +class SubmissionsCountFilter(InputFilter): # Human-readable title which will be displayed in the # right admin sidebar just above the filter options. - title = _("Private non-test") - + title = _("Submissions Count") # Parameter for the filter that will be used in the URL query. - parameter_name = "private" + parameter_name = "submissions_count_gte" + + def queryset(self, request, queryset): + if self.value() is not None: + value = self.value() + return queryset.filter(submissions_count__gte=value) - def lookups(self, request, model_admin): - """ - Returns a list of tuples. The first element in each - tuple is the coded value for the option that will - appear in the URL query. The second element is the - human-readable name for the option that will appear - in the right sidebar. - """ - return [ - ("privateSmall", _("Submissions >= 10 and Participants >= 5")), - ] + +class ParticipantsCountFilter(InputFilter): + title = _("Participants Count") + parameter_name = "participants_count_gte" def queryset(self, request, queryset): - """ - Returns the filtered queryset based on the value - provided in the query string and retrievable via - `self.value()`. - """ - # Only show private competitions with >= 10 submissions and >=5 participants - if self.value() == "privateSmall": - return queryset.filter( - published=False, submissions_count__gte=10, participants_count__gte=5 - ) + if self.value() is not None: + value = self.value() + return queryset.filter(participants_count__gte=value) # This will export the email of all the selected competition creators, removing duplications and banned users @@ -85,13 +94,35 @@ def export_as_json(modeladmin, request, queryset): return HttpResponse(json.dumps(email_list), content_type="application/json") +class QueueFilter(InputFilter): + # Human-readable title which will be displayed in the + # right admin sidebar just above the filter options. + title = _("Queue (default for Default Queue)") + # Parameter for the filter that will be used in the URL query. + parameter_name = "queue" + + def queryset(self, request, queryset): + if self.value() is not None: + value = self.value() + if value.lower() == "default": + return queryset.filter(queue__name__isnull=True) + else: + return queryset.filter(queue__name=value) + + class CompetitionExpansion(admin.ModelAdmin): - search_fields = ["title", "docker_image", "created_by__username"] - list_display = ["id", "title", "created_by", "is_featured"] + search_fields = ["id", "title", "docker_image", "created_by__username"] + list_display = ["id", "title", "created_by", "published", "is_featured"] list_display_links = ["id", "title"] actions = [export_as_json, export_as_csv] raw_id_fields = ["created_by", "collaborators", "queue"] - list_filter = ["is_featured", privateCompetitionsFilter] + list_filter = [ + "published", + "is_featured", + ParticipantsCountFilter, + SubmissionsCountFilter, + QueueFilter, + ] fieldsets = [ ( None, @@ -136,6 +167,35 @@ class CompetitionExpansion(admin.ModelAdmin): ] +class CompetitionOrganizerFilter(InputFilter): + # Human-readable title which will be displayed in the + # right admin sidebar just above the filter options. + title = _("Competition Organizer") + # Parameter for the filter that will be used in the URL query. + parameter_name = "organizer" + + def queryset(self, request, queryset): + if self.value() is not None: + value = self.value() + return queryset.filter(phase__competition__created_by__username=value) + + +class SubmissionQueueFilter(InputFilter): + # Human-readable title which will be displayed in the + # right admin sidebar just above the filter options. + title = _("Queue (default for Default Queue)") + # Parameter for the filter that will be used in the URL query. + parameter_name = "queue" + + def queryset(self, request, queryset): + if self.value() is not None: + value = self.value() + if value.lower() == "default": + return queryset.filter(phase__competition__queue__name__isnull=True) + else: + return queryset.filter(phase__competition__queue__name=value) + + class SubmissionExpansion(admin.ModelAdmin): # Raw Id Fields changes the field from displaying everything in a drop down menu into an id fields, which makes the page loads much faster (removes huge SELECT from the database) raw_id_fields = [ @@ -151,16 +211,24 @@ class SubmissionExpansion(admin.ModelAdmin): "parent", "scores", ] - search_fields = ["owner__username"] + search_fields = ["id", "owner__username", "phase__competition__title", "task__name"] list_display = [ "id", "owner", "task", + "status", + "is_public", + "has_children", + "is_soft_deleted", + ] + list_filter = [ "is_public", "has_children", "is_soft_deleted", + "status", + CompetitionOrganizerFilter, + SubmissionQueueFilter, ] - list_filter = ["is_public", "has_children", "is_soft_deleted"] fieldsets = [ ( None, @@ -232,13 +300,13 @@ class CompetitionParticipantExpansion(admin.ModelAdmin): raw_id_fields = ["user", "competition"] list_display = ["id", "user", "competition", "status"] list_filter = ["status"] - search_fields = ["user__username", "competition"] + search_fields = ["id", "user__username", "competition"] class PageExpansion(admin.ModelAdmin): raw_id_fields = ["competition"] list_display = ["id", "competition"] - search_fields = ["competition", "content"] + search_fields = ["id", "competition", "content"] class PhaseExpansion(admin.ModelAdmin): diff --git a/src/apps/forums/admin.py b/src/apps/forums/admin.py index cc49d434e..1c9a38efc 100644 --- a/src/apps/forums/admin.py +++ b/src/apps/forums/admin.py @@ -24,20 +24,20 @@ def DeactivateAccountPost(modeladmin, request, queryset): class ForumsExpansion(admin.ModelAdmin): raw_id_fields = ["competition"] list_display = ["id", "competition"] - search_fields = ["competition"] + search_fields = ["id", "competition"] class ThreadExpansion(admin.ModelAdmin): raw_id_fields = ["forum", "started_by"] list_display = ["id", "title", "started_by"] - search_fields = ["title", "started_by__username"] + search_fields = ["id", "title", "started_by__username"] actions = [DeactivateAccountThread] class PostExpansion(admin.ModelAdmin): raw_id_fields = ["thread", "posted_by"] list_display = ["id", "content_limited", "posted_by"] - search_fields = ["content", "posted_by__username"] + search_fields = ["id", "content", "posted_by__username"] actions = [DeactivateAccountPost] @admin.display(description="Content", ordering="content") diff --git a/src/apps/oidc_configurations/admin.py b/src/apps/oidc_configurations/admin.py index 5ea6e683f..2b606b8a1 100644 --- a/src/apps/oidc_configurations/admin.py +++ b/src/apps/oidc_configurations/admin.py @@ -1,6 +1,10 @@ from django.contrib import admin from .models import Auth_Organization -admin.site.register(Auth_Organization) -# Register your models here. +class Auth_OrganizationExpansion(admin.ModelAdmin): + list_display = ["id", "name", "client_id"] + search_fields = ["id", "name", "client_id"] + + +admin.site.register(Auth_Organization, Auth_OrganizationExpansion) diff --git a/src/apps/profiles/admin.py b/src/apps/profiles/admin.py index ba6868ef4..4447f36d6 100644 --- a/src/apps/profiles/admin.py +++ b/src/apps/profiles/admin.py @@ -1,15 +1,54 @@ from django.contrib import admin - from .models import User, DeletedUser, Organization, Membership +from django.utils.translation import gettext_lazy as _ + + +# General class used to make custom filter +class InputFilter(admin.SimpleListFilter): + template = "admin/input_filter.html" + + def lookups(self, request, model_admin): + # Dummy, required to show the filter. + return ((),) + + def choices(self, changelist): + # Grab only the "all" option. + all_choice = next(super().choices(changelist)) + all_choice["query_parts"] = ( + (k, v) + for k, v in changelist.get_filters_params().items() + if k != self.parameter_name + ) + yield all_choice + + +class QuotaFilter(InputFilter): + # Human-readable title which will be displayed in the + # right admin sidebar just above the filter options. + title = _("Quota") + # Parameter for the filter that will be used in the URL query. + parameter_name = "quota_gte" + + def queryset(self, request, queryset): + if self.value() is not None: + value = self.value() + return queryset.filter(quota__gte=value) class UserExpansion(admin.ModelAdmin): # The following two lines are needed for Django-su: change_form_template = "admin/auth/user/change_form.html" change_list_template = "admin/auth/user/change_list.html" - search_fields = ["username", "email", "id"] - list_filter = ["is_staff", "is_superuser", "is_deleted", "is_bot", "is_banned"] - list_display = ["id", "username", "email", "is_staff", "is_superuser", "is_banned"] + search_fields = ["id", "username", "email"] + list_filter = [ + "is_staff", + "is_superuser", + "is_deleted", + "is_bot", + "is_banned", + QuotaFilter, + ] + list_display = ["id", "username", "email", "quota", "is_staff", "is_superuser", "is_banned"] list_display_links = ["id", "username"] raw_id_fields = ["oidc_organization", "groups"] fieldsets = [ @@ -93,7 +132,7 @@ class UserExpansion(admin.ModelAdmin): class DeletedUserExpansion(admin.ModelAdmin): list_display = ("user_id", "username", "email", "deleted_at") - search_fields = ("username", "email") + search_fields = ("id", "username", "email") list_filter = ("deleted_at",) diff --git a/src/apps/queues/admin.py b/src/apps/queues/admin.py index ce1029831..a0410d0fb 100644 --- a/src/apps/queues/admin.py +++ b/src/apps/queues/admin.py @@ -40,7 +40,7 @@ class QueueExpansion(admin.ModelAdmin): list_display = ["id", "name", "owner", "is_public"] list_display_links = ["id", "name"] list_filter = ["is_public"] - search_fields = ["name", "owner__username", "organizers__username"] + search_fields = ["id", "name", "owner__username", "organizers__username"] actions = [export_as_csv, export_as_json] diff --git a/src/templates/admin/input_filter.html b/src/templates/admin/input_filter.html new file mode 100644 index 000000000..65b8f3786 --- /dev/null +++ b/src/templates/admin/input_filter.html @@ -0,0 +1,25 @@ +{% load i18n %} + +

{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}

+
    +
  • + {% with choices.0 as all_choice %} +
    + + {% for k, v in all_choice.query_parts %} + + {% endfor %} + + + + {% if not all_choice.selected %} + x {% trans 'Remove' %} + {% endif %} + +
    + {% endwith %} +
  • +
+ From 82d148cf24e04cb9bbed63eedc9c6f08665b57d9 Mon Sep 17 00:00:00 2001 From: Obada Haddad Date: Fri, 26 Dec 2025 10:20:10 +0100 Subject: [PATCH 9/9] make some filter clearer: remove useless repetitions --- src/apps/competitions/admin.py | 46 ++++++++++++++++++---------------- src/apps/profiles/admin.py | 2 +- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/apps/competitions/admin.py b/src/apps/competitions/admin.py index c7adbe1d4..a7b4fe4fd 100644 --- a/src/apps/competitions/admin.py +++ b/src/apps/competitions/admin.py @@ -29,7 +29,7 @@ def choices(self, changelist): class SubmissionsCountFilter(InputFilter): # Human-readable title which will be displayed in the # right admin sidebar just above the filter options. - title = _("Submissions Count") + title = _("≥ Submissions Count (Greater than or Equal to)") # Parameter for the filter that will be used in the URL query. parameter_name = "submissions_count_gte" @@ -40,7 +40,7 @@ def queryset(self, request, queryset): class ParticipantsCountFilter(InputFilter): - title = _("Participants Count") + title = _("≥ Participants Count (Greater Than or Equal to)") parameter_name = "participants_count_gte" def queryset(self, request, queryset): @@ -50,7 +50,8 @@ def queryset(self, request, queryset): # This will export the email of all the selected competition creators, removing duplications and banned users -def export_as_csv(modeladmin, request, queryset): +@admin.display(description="Export as CSV") +def CompetitionExport_as_csv(modeladmin, request, queryset): response = HttpResponse( content_type="text/csv", headers={ @@ -84,8 +85,24 @@ def export_as_csv(modeladmin, request, queryset): return response +@admin.display(description="Export as CSV") +def SubmissionsExport_as_csv(modeladmin, request, queryset): + response = HttpResponse( + content_type="text/csv", + headers={ + "Content-Disposition": 'attachment; filename="submissions.csv"' + }, + ) + writer = csv.writer(response) + writer.writerow(["ID", "Owner", "Status", "Task", "PhaseQueue"]) + for obj in queryset: + writer.writerow([obj.id, obj.owner, obj.status, obj.task, obj.phase, obj.queue]) + return response + + # This will export the email of all the selected competition creators, removing duplications and banned users -def export_as_json(modeladmin, request, queryset): +@admin.display(description="Export as JSON") +def CompetitionExport_as_json(modeladmin, request, queryset): email_list = {} for obj in queryset: user = User.objects.get(id=obj.created_by_id) @@ -114,7 +131,7 @@ class CompetitionExpansion(admin.ModelAdmin): search_fields = ["id", "title", "docker_image", "created_by__username"] list_display = ["id", "title", "created_by", "published", "is_featured"] list_display_links = ["id", "title"] - actions = [export_as_json, export_as_csv] + actions = [CompetitionExport_as_json, CompetitionExport_as_csv] raw_id_fields = ["created_by", "collaborators", "queue"] list_filter = [ "published", @@ -180,22 +197,6 @@ def queryset(self, request, queryset): return queryset.filter(phase__competition__created_by__username=value) -class SubmissionQueueFilter(InputFilter): - # Human-readable title which will be displayed in the - # right admin sidebar just above the filter options. - title = _("Queue (default for Default Queue)") - # Parameter for the filter that will be used in the URL query. - parameter_name = "queue" - - def queryset(self, request, queryset): - if self.value() is not None: - value = self.value() - if value.lower() == "default": - return queryset.filter(phase__competition__queue__name__isnull=True) - else: - return queryset.filter(phase__competition__queue__name=value) - - class SubmissionExpansion(admin.ModelAdmin): # Raw Id Fields changes the field from displaying everything in a drop down menu into an id fields, which makes the page loads much faster (removes huge SELECT from the database) raw_id_fields = [ @@ -212,6 +213,7 @@ class SubmissionExpansion(admin.ModelAdmin): "scores", ] search_fields = ["id", "owner__username", "phase__competition__title", "task__name"] + actions = [SubmissionsExport_as_csv] list_display = [ "id", "owner", @@ -227,7 +229,7 @@ class SubmissionExpansion(admin.ModelAdmin): "is_soft_deleted", "status", CompetitionOrganizerFilter, - SubmissionQueueFilter, + QueueFilter, ] fieldsets = [ ( diff --git a/src/apps/profiles/admin.py b/src/apps/profiles/admin.py index 4447f36d6..3715c59c7 100644 --- a/src/apps/profiles/admin.py +++ b/src/apps/profiles/admin.py @@ -25,7 +25,7 @@ def choices(self, changelist): class QuotaFilter(InputFilter): # Human-readable title which will be displayed in the # right admin sidebar just above the filter options. - title = _("Quota") + title = _("≥ Quota (Greater than or Equal to)") # Parameter for the filter that will be used in the URL query. parameter_name = "quota_gte"