Skip to content

Commit

Permalink
Merge pull request #1345 from codalab/develop
Browse files Browse the repository at this point in the history
Merge develop into master
  • Loading branch information
Didayolo authored Feb 22, 2024
2 parents e5cad74 + dba7623 commit b650ce8
Show file tree
Hide file tree
Showing 28 changed files with 360 additions and 85 deletions.
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

0 comments on commit b650ce8

Please sign in to comment.