Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2546aa0
rerun whole phase, only rerun submissions with no parent (this automa…
ihsaan-ullah Feb 11, 2024
5179ec3
add message in the empty error
ihsaan-ullah Feb 11, 2024
35eaaee
unused import removed
ihsaan-ullah Feb 11, 2024
3513e9e
test fixed, additional test added
ihsaan-ullah Feb 12, 2024
b1d549d
leaderboard number of enteries: exlude failed and cancelled submissions
ihsaan-ullah Feb 14, 2024
52b84d4
Merge pull request #1330 from codalab/leaderboard_num_submissions
Didayolo Feb 15, 2024
bfce6b5
Merge pull request #1323 from codalab/rerun_all_submissions
Didayolo Feb 15, 2024
16579a7
empty error is now shown to participant
ihsaan-ullah Feb 15, 2024
fca617e
empty error resolved
ihsaan-ullah Feb 16, 2024
18f521b
small logo for popular and featured comps on home page
ihsaan-ullah Feb 16, 2024
1490108
privacy link updated
ihsaan-ullah Feb 16, 2024
76e4d2b
html tags in email text
ihsaan-ullah Feb 17, 2024
d38e74c
Merge pull request #1336 from codalab/small_logo
Didayolo Feb 19, 2024
4e5e085
Merge pull request #1324 from codalab/submission_leaderboard
Didayolo Feb 19, 2024
5e424c8
Merge pull request #1339 from codalab/privacy_link
Didayolo Feb 19, 2024
a3787e4
removed caching competitions for the public page
ihsaan-ullah Feb 20, 2024
24526da
unused imports removed
ihsaan-ullah Feb 20, 2024
a4ff842
Merge pull request #1343 from codalab/public_page_comp
Didayolo Feb 21, 2024
68c4983
PhaseSerializer is_final_phase change
bbearce Feb 21, 2024
1ca4c3e
Merge pull request #1344 from codalab/issue_1180_manual_ui_creation
Didayolo Feb 21, 2024
720f014
Add email in .env_sample
Didayolo Feb 22, 2024
6826930
Competition show user display name if available (#1321)
ihsaan-ullah Feb 22, 2024
88c8a89
Competition Auto-Run Submissions (#1294)
ihsaan-ullah Feb 22, 2024
b12fb43
Merge pull request #1340 from codalab/user_email
Didayolo Feb 22, 2024
dba7623
Merge Django migrations
Didayolo Feb 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env_sample
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ SELENIUM_HOSTNAME=selenium
#EMAIL_HOST_PASSWORD=pass
#EMAIL_PORT=587
#EMAIL_USE_TLS=True
#DEFAULT_FROM_EMAIL="Codabench <noreply@example.com>"
#SERVER_EMAIL=noreply@example.com

# -----------------------------------------------------------------------------
# Storage
Expand Down
32 changes: 30 additions & 2 deletions src/apps/api/serializers/competitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class PhaseSerializer(WritableNestedModelSerializer):
tasks = serializers.SlugRelatedField(queryset=Task.objects.all(), required=True, allow_null=False, slug_field='key',
many=True)
status = serializers.SerializerMethodField()
is_final_phase = serializers.SerializerMethodField()

class Meta:
model = Phase
Expand All @@ -48,6 +49,14 @@ class Meta:
'is_final_phase',
)

def get_is_final_phase(self, obj):
if len(obj.competition.phases.all()) > 1:
return obj.is_final_phase
elif len(obj.competition.phases.all()) == 1:
obj.is_final_phase = True
obj.save()
return obj.is_final_phase

def get_status(self, obj):

now = datetime.now().replace(tzinfo=None)
Expand Down Expand Up @@ -245,6 +254,7 @@ class Meta:
'registration_auto_approve',
'queue',
'enable_detailed_results',
'auto_run_submissions',
'make_programs_available',
'make_input_data_available',
'docker_image',
Expand Down Expand Up @@ -327,6 +337,7 @@ class CompetitionCreateSerializer(CompetitionSerializer):

class CompetitionDetailSerializer(serializers.ModelSerializer):
created_by = serializers.CharField(source='created_by.username', read_only=True)
owner_display_name = serializers.SerializerMethodField()
logo_icon = NamedBase64ImageField(allow_null=True)
pages = PageSerializer(many=True)
phases = PhaseDetailSerializer(many=True)
Expand All @@ -346,6 +357,7 @@ class Meta:
'published',
'secret_key',
'created_by',
'owner_display_name',
'created_when',
'logo',
'logo_icon',
Expand All @@ -361,6 +373,7 @@ class Meta:
'submission_count',
'queue',
'enable_detailed_results',
'auto_run_submissions',
'make_programs_available',
'make_input_data_available',
'docker_image',
Expand All @@ -371,7 +384,7 @@ class Meta:
'reward',
'contact_email',
'report',
'whitelist_emails'
'whitelist_emails',
)

def get_leaderboards(self, instance):
Expand All @@ -389,9 +402,14 @@ def get_whitelist_emails(self, instance):
whitelist_emails_list = [entry.email for entry in whitelist_emails_query]
return whitelist_emails_list

def get_owner_display_name(self, obj):
# Get the user's display name if not None, otherwise return username
return obj.created_by.display_name if obj.created_by.display_name else obj.created_by.username


class CompetitionSerializerSimple(serializers.ModelSerializer):
created_by = serializers.CharField(source='created_by.username')
created_by = serializers.CharField(source='created_by.username', read_only=True)
owner_display_name = serializers.SerializerMethodField()
participant_count = serializers.IntegerField(read_only=True)

class Meta:
Expand All @@ -400,17 +418,27 @@ class Meta:
'id',
'title',
'created_by',
'owner_display_name',
'created_when',
'published',
'participant_count',
'logo',
'logo_icon',
'description',
'competition_type',
'reward',
'contact_email',
'report',
)

def get_created_by(self, obj):
# Get the user's display name if not None, otherwise return username
return obj.created_by.display_name if obj.created_by.display_name else obj.created_by.username

def get_owner_display_name(self, obj):
# Get the user's display name if not None, otherwise return username
return obj.created_by.display_name if obj.created_by.display_name else obj.created_by.username


PageSerializer.competition = CompetitionSerializer(many=True, source='competition')

Expand Down
7 changes: 6 additions & 1 deletion src/apps/api/serializers/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ class Meta:


class DataDetailSerializer(serializers.ModelSerializer):
created_by = serializers.CharField(source='created_by.username')
created_by = serializers.CharField(source='created_by.username', read_only=True)
owner_display_name = serializers.SerializerMethodField()
competition = serializers.SerializerMethodField()
value = serializers.CharField(source='key', required=False)

Expand All @@ -83,6 +84,7 @@ class Meta:
fields = (
'id',
'created_by',
'owner_display_name',
'created_when',
'name',
'type',
Expand All @@ -108,6 +110,9 @@ def get_competition(self, obj):
}
return None

def get_owner_display_name(self, instance):
return instance.created_by.display_name if instance.created_by.display_name else instance.created_by.username


class DataGroupSerializer(serializers.ModelSerializer):
class Meta:
Expand Down
13 changes: 11 additions & 2 deletions src/apps/api/serializers/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class SubmissionSerializer(serializers.ModelSerializer):
on_leaderboard = serializers.BooleanField(read_only=True)
task = TaskSerializer()
created_when = serializers.DateTimeField(format="%Y-%m-%d %H:%M")
auto_run = serializers.SerializerMethodField(read_only=True)

class Meta:
model = Submission
Expand All @@ -66,6 +67,7 @@ class Meta:
'leaderboard',
'on_leaderboard',
'task',
'auto_run'
)
read_only_fields = (
'pk',
Expand All @@ -79,6 +81,10 @@ class Meta:
def get_filename(self, instance):
return basename(instance.data.data_file.name)

def get_auto_run(self, instance):
# returns this submission's competition auto_run_submissions Flag
return instance.phase.competition.auto_run_submissions


class SubmissionLeaderBoardSerializer(serializers.ModelSerializer):
scores = SubmissionScoreSerializer(many=True)
Expand Down Expand Up @@ -151,9 +157,12 @@ def get_filename(self, instance):

def create(self, validated_data):
tasks = validated_data.pop('tasks', None)

sub = super().create(validated_data)
sub.start(tasks=tasks)

# Check if auto_run_submissions is enabled then run the submission
# Otherwise organizer will run manually
if sub.phase.competition.auto_run_submissions:
sub.start(tasks=tasks)

return sub

Expand Down
15 changes: 14 additions & 1 deletion src/apps/api/serializers/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ def get_validated(self, instance):


class TaskDetailSerializer(WritableNestedModelSerializer):
created_by = serializers.CharField(source='created_by.username', read_only=True, required=False)
created_by = serializers.CharField(source='created_by.username', read_only=True)
owner_display_name = serializers.SerializerMethodField()
input_data = DataSimpleSerializer(read_only=True)
ingestion_program = DataSimpleSerializer(read_only=True)
reference_data = DataSimpleSerializer(read_only=True)
Expand All @@ -107,6 +108,7 @@ class Meta:
'description',
'key',
'created_by',
'owner_display_name',
'created_when',
'is_public',
'validated',
Expand All @@ -126,19 +128,26 @@ def get_validated(self, task):
def get_shared_with(self, instance):
return self.context['shared_with'][instance.pk]

def get_owner_display_name(self, instance):
# Get the user's display name if not None, otherwise return username
return instance.created_by.display_name if instance.created_by.display_name else instance.created_by.username


class TaskListSerializer(serializers.ModelSerializer):
solutions = SolutionListSerializer(many=True, required=False, read_only=True)
value = serializers.CharField(source='key', required=False)
competitions = serializers.SerializerMethodField()
shared_with = serializers.SerializerMethodField()
created_by = serializers.CharField(source='created_by.username', read_only=True)
owner_display_name = serializers.SerializerMethodField()

class Meta:
model = Task
fields = (
'id',
'created_when',
'created_by',
'owner_display_name',
'key',
'name',
'solutions',
Expand All @@ -160,6 +169,10 @@ def get_competitions(self, instance):
def get_shared_with(self, instance):
return self.context['shared_with'][instance.pk]

def get_owner_display_name(self, instance):
# Get the user's display name if not None, otherwise return username
return instance.created_by.display_name if instance.created_by.display_name else instance.created_by.username


class PhaseTaskInstanceSerializer(serializers.HyperlinkedModelSerializer):
task = serializers.SlugRelatedField(queryset=Task.objects.all(), required=True, allow_null=False, slug_field='key',
Expand Down
19 changes: 11 additions & 8 deletions src/apps/api/views/competitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
from rest_framework_csv.renderers import CSVRenderer
from rest_framework_extensions.cache.decorators import cache_response
from rest_framework_extensions.key_constructor.constructors import DefaultListKeyConstructor
from api.pagination import LargePagination
from api.renderers import ZipRenderer
from rest_framework.viewsets import ModelViewSet
Expand Down Expand Up @@ -533,7 +531,6 @@ def create_dump(self, request, pk=None):
serializer = CompetitionCreationTaskStatusSerializer({"status": "Success. Competition dump is being created."})
return Response(serializer.data, status=201)

@cache_response(key_func=DefaultListKeyConstructor())
@action(detail=False, methods=('GET',), pagination_class=LargePagination)
def public(self, request):
qs = self.get_queryset()
Expand Down Expand Up @@ -634,8 +631,8 @@ def rerun_submissions(self, request, pk):
phase = self.get_object()
comp = phase.competition

# Get submissions
submissions = phase.submissions.all()
# Get submissions with no parent
submissions = phase.submissions.filter(parent__isnull=True)

can_re_run_submissions = False
error_message = ""
Expand Down Expand Up @@ -704,12 +701,18 @@ def get_leaderboard(self, request, pk):
submission_detailed_results = {}
for submission in query['submissions']:
# count number of entries/number of submissions for the owner of this submission for this phase
# count all submissions with no parent and count all parents without counting the children
# count all submissions except:
# - child submissions (submissions who has a parent i.e. parent field is not null)
# - Failed submissions
# - Cancelled submissions
num_entries = Submission.objects.filter(
Q(owner__username=submission['owner']) | Q(parent__owner__username=submission['owner']),
Q(owner__username=submission['owner']) |
Q(parent__owner__username=submission['owner']),
phase=phase,
).exclude(
parent__isnull=False
Q(status=Submission.FAILED) |
Q(status=Submission.CANCELLED) |
Q(parent__isnull=False)
).count()

submission_key = f"{submission['owner']}{submission['parent'] or submission['id']}"
Expand Down
38 changes: 31 additions & 7 deletions src/apps/api/views/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes, action
from django.http import Http404
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.filters import SearchFilter
from rest_framework.generics import get_object_or_404
Expand Down Expand Up @@ -93,7 +92,7 @@ def check_object_permissions(self, request, obj):

not_bot_user = self.request.user.is_authenticated and not self.request.user.is_bot

if self.action in ['update_fact_sheet', 're_run_submission']:
if self.action in ['update_fact_sheet', 'run_submission', 're_run_submission']:
# get_queryset will stop us from re-running something we're not supposed to
pass
elif not self.request.user.is_authenticated or not_bot_user:
Expand Down Expand Up @@ -206,14 +205,24 @@ def has_admin_permission(self, user, submission):

@action(detail=True, methods=('POST', 'DELETE'))
def submission_leaderboard_connection(self, request, pk):

# get submission
submission = self.get_object()

# get submission phase
phase = submission.phase

if not (request.user.is_superuser or request.user == submission.owner):
if not phase.competition.collaborators.filter(pk=request.user.pk).exists():
raise Http404
# only super user, owner of submission and competition organizer can proceed
if not (
request.user.is_superuser or
request.user == submission.owner or
request.user in phase.competition.all_organizers
):
raise PermissionDenied("You cannot perform this action, contact the competition organizer!")

# only super user and with these leaderboard rules (FORCE_LAST, FORCE_BEST, FORCE_LATEST_MULTIPLE) can proceed
if submission.phase.leaderboard.submission_rule in Leaderboard.AUTO_SUBMISSION_RULES and not request.user.is_superuser:
raise ValidationError("Users are not allowed to edit the leaderboard on this Competition")
raise PermissionDenied("Users are not allowed to edit the leaderboard on this Competition")

if request.method == 'POST':
# Removing any existing submissions on leaderboard unless multiples are allowed
Expand All @@ -228,7 +237,7 @@ def submission_leaderboard_connection(self, request, pk):

if request.method == 'DELETE':
if submission.phase.leaderboard.submission_rule not in [Leaderboard.ADD_DELETE, Leaderboard.ADD_DELETE_MULTIPLE]:
raise ValidationError("You are not allowed to remove a submission on this phase")
raise PermissionDenied("You are not allowed to remove a submission on this phase")
submission.leaderboard = None
submission.save()
Submission.objects.filter(parent=submission).update(leaderboard=None)
Expand All @@ -246,6 +255,21 @@ def cancel_submission(self, request, pk):
canceled = submission.cancel()
return Response({'canceled': canceled})

@action(detail=True, methods=('POST',))
def run_submission(self, request, pk):
submission = self.get_object()

# Only organizer of the competition can run the submission
if not self.has_admin_permission(request.user, submission):
raise PermissionDenied('You do not have permission to run this submission')

# Allow only to run a submission with status `Submitting`
if submission.status != Submission.SUBMITTING:
raise PermissionDenied('Cannot run a submission which is not in submitting status')

new_sub = submission.run()
return Response({'id': new_sub.id})

@action(detail=True, methods=('POST',))
def re_run_submission(self, request, pk):
submission = self.get_object()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.17 on 2024-01-22 10:24

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('competitions', '0044_merge_20231221_1416'),
]

operations = [
migrations.AddField(
model_name='competition',
name='auto_run_submissions',
field=models.BooleanField(default=True),
),
]
Loading