Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions src/apps/api/serializers/competitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ class Meta:
'allow_robot_submissions',
'competition_type',
'fact_sheet',
'reward',
'contact_email',
'report',
)

def validate_phases(self, phases):
Expand Down Expand Up @@ -330,6 +333,9 @@ class Meta:
'competition_type',
'fact_sheet',
'forum',
'reward',
'contact_email',
'report',
)

def get_leaderboards(self, instance):
Expand Down Expand Up @@ -359,6 +365,9 @@ class Meta:
'logo',
'description',
'competition_type',
'reward',
'contact_email',
'report',
)


Expand Down
11 changes: 10 additions & 1 deletion src/apps/api/serializers/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from utils.data import make_url_sassy

from tasks.models import Task
from queues.models import Queue


class SubmissionScoreSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -113,6 +114,7 @@ class SubmissionCreationSerializer(DefaultUserCreateMixin, serializers.ModelSeri
filename = serializers.SerializerMethodField(read_only=True)
tasks = serializers.PrimaryKeyRelatedField(queryset=Task.objects.all(), required=False, write_only=True, many=True)
phase = serializers.PrimaryKeyRelatedField(queryset=Phase.objects.all(), required=True)
queue = serializers.PrimaryKeyRelatedField(queryset=Queue.objects.all(), required=False, allow_null=True)

class Meta:
model = Submission
Expand All @@ -130,6 +132,7 @@ class Meta:
'tasks',
'fact_sheet_answers',
'organization',
'queue'
)
extra_kwargs = {
'secret': {"write_only": True},
Expand Down Expand Up @@ -181,10 +184,16 @@ def validate(self, attrs):
return data

def update(self, submission, validated_data):
# TODO: Test, could you change the phase of a submission?

# Cannot change submission if secret key is not valid
if submission.secret != validated_data.get('secret'):
raise PermissionDenied("Submission secret invalid")

# Task of a submission cannot be updated
if "task" in validated_data:
raise PermissionDenied("Task of a submission cannot be update")

# Update status if it is there in validated data
if "status" in validated_data:
# Received a status update, let the frontend know
from channels.layers import get_channel_layer
Expand Down
13 changes: 12 additions & 1 deletion src/apps/api/tests/test_leaderboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,18 @@ def test_getting_many_submissions_doesnt_cause_too_many_queries(self):
assert resp.status_code == 200


# TODO: Test listing all leaderboards isn't a thing?
class LeaderboardTest(APITestCase):
def setUp(self):
leaderboard1 = factories.LeaderboardFactory()
leaderboard2 = factories.LeaderboardFactory()
_ = factories.ColumnFactory(leaderboard=leaderboard1, index=0)
_ = factories.ColumnFactory(leaderboard=leaderboard2, index=0)

def test_get_all_leaderboards(self):
url = reverse('leaderboard-list')
resp = self.client.get(url)
assert resp.status_code == 200
assert resp.data == []


class HiddenLeaderboardTests(APITestCase):
Expand Down
28 changes: 28 additions & 0 deletions src/apps/api/tests/test_submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,34 @@ def test_who_can_see_detailed_result_when_visualization_is_true(self):
assert resp.status_code == 403


class SubmissionUpdateTest(APITestCase):
def setUp(self):
self.user = UserFactory(username='test')
self.task1 = TaskFactory(created_by=self.user)
self.task2 = TaskFactory(created_by=self.user)
self.competition = CompetitionFactory(created_by=self.user)
self.phase = PhaseFactory(competition=self.competition, tasks=[self.task1])
self.secret = '7df3600c-1234-5678-bbc8-bbe91f42d875'
self.submission = SubmissionFactory(
task=self.task1,
phase=self.phase,
status=Submission.FINISHED,
secret=self.secret
)

def test_submission_task_update(self):
url = reverse('submission-detail', args=(self.submission.pk,))

# Update task
resp = self.client.patch(url, {
"task": self.task2.id,
"secret": self.secret
})
assert resp.status_code == 403
assert resp.data["detail"] == "Submission task cannot be updated"
assert self.submission.task.id == self.task1.id # task not updated


class OrganizationSubmissionTests(APITestCase):
def setUp(self):
# Competition and creator
Expand Down
14 changes: 13 additions & 1 deletion src/apps/api/views/competitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,15 @@ def results(self, request, pk, format=None):
raise PermissionDenied("You are not a competition admin or superuser")
selected_phase = request.GET.get('phase')
data = self.collect_leaderboard_data(competition, selected_phase)

if format == 'zip':
with SpooledTemporaryFile() as tmp:
with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as archive:
for leaderboard in data:

# Check if the leaderboard is empty
if not data[leaderboard]:
continue

stringIO = StringIO()
columns = list(data[leaderboard][list(data[leaderboard].keys())[0]])
dict_writer = csv.DictWriter(stringIO, fieldnames=(["Username", "fact_sheet_answers"] + columns))
Expand All @@ -437,6 +441,14 @@ def results(self, request, pk, format=None):
elif len(data) == 0:
raise ValidationError("No Matching Leaderboard")
leaderboard_title = list(data.keys())[0]

# Check if the leaderboard data is empty
if not data[leaderboard_title]:
# Return an empty CSV
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="{leaderboard_title}.csv"'
return response

response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="{leaderboard_title}.csv"'
columns = list(data[leaderboard_title][list(data[leaderboard_title].keys())[0]].keys())
Expand Down
5 changes: 5 additions & 0 deletions src/apps/api/views/leaderboards.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from rest_framework.response import Response
from api.permissions import LeaderboardNotHidden, LeaderboardIsOrganizerOrCollaborator
from api.serializers.leaderboards import LeaderboardEntriesSerializer
from api.serializers.submissions import SubmissionScoreSerializer
Expand All @@ -23,6 +24,10 @@ def get_permissions(self):

return [permission() for permission in self.permission_classes]

def list(self, request, *args, **kwargs):
# Return an empty list for the leaderboard-list endpoint
return Response([])


class SubmissionScoreViewSet(ModelViewSet):
queryset = SubmissionScore.objects.all()
Expand Down
2 changes: 2 additions & 0 deletions src/apps/api/views/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def check_object_permissions(self, request, obj):
try:
if request.data.get('secret') is None or uuid.UUID(request.data.get('secret')) != obj.secret:
raise PermissionDenied("Submission secrets do not match")
if request.data.get('task'):
raise PermissionDenied("Submission task cannot be updated")
except TypeError:
raise ValidationError(f"Secret: ({request.data.get('secret')}) not a valid UUID")

Expand Down
17 changes: 9 additions & 8 deletions src/apps/competitions/migrations/0026_auto_20201110_1932.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
# Generated by Django 2.2.13 on 2020-11-10 19:32
# Manual edit on 2023-08-22 by Adrien Pavao

import django.contrib.postgres.fields.jsonb
from django.db import migrations

from competitions.models import Competition

def set_blank_fact_sheet_to_null(apps, schema_editor):
Competition = apps.get_model('competitions', 'Competition')
for comp in Competition.objects.all():
if comp.fact_sheet == "":
comp.fact_sheet = None
comp.save()

class Migration(migrations.Migration):

def set_blank_fact_sheet_to_null(self, schema_editor):
for comp in Competition.objects.all():
if comp.fact_sheet == "":
comp.fact_sheet = None
comp.save()
class Migration(migrations.Migration):

dependencies = [
('competitions', '0025_submission_is_specific_task_re_run'),
Expand All @@ -24,5 +25,5 @@ def set_blank_fact_sheet_to_null(self, schema_editor):
name='fact_sheet',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, max_length=4096, null=True),
),
migrations.RunPython(set_blank_fact_sheet_to_null),
migrations.RunPython(set_blank_fact_sheet_to_null, migrations.RunPython.noop),
]
28 changes: 28 additions & 0 deletions src/apps/competitions/migrations/0035_auto_20230806_0715.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 2.2.17 on 2023-08-06 07:15

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('competitions', '0034_auto_20230727_1147'),
]

operations = [
migrations.AddField(
model_name='competition',
name='contact_email',
field=models.EmailField(blank=True, max_length=256, null=True),
),
migrations.AddField(
model_name='competition',
name='report',
field=models.CharField(blank=True, max_length=256, null=True),
),
migrations.AddField(
model_name='competition',
name='reward',
field=models.CharField(blank=True, max_length=256, null=True),
),
]
20 changes: 20 additions & 0 deletions src/apps/competitions/migrations/0036_submission_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 2.2.17 on 2023-08-24 07:44

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('queues', '0001_initial'),
('competitions', '0035_auto_20230806_0715'),
]

operations = [
migrations.AddField(
model_name='submission',
name='queue',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submissions', to='queues.Queue'),
),
]
6 changes: 6 additions & 0 deletions src/apps/competitions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class Competition(ChaHubSaveMixin, models.Model):

fact_sheet = JSONField(blank=True, null=True, max_length=4096, default=None)

contact_email = models.EmailField(max_length=256, null=True, blank=True)
reward = models.CharField(max_length=256, null=True, blank=True)
report = models.CharField(max_length=256, null=True, blank=True)

def __str__(self):
return f"competition-{self.title}-{self.pk}-{self.competition_type}"

Expand Down Expand Up @@ -456,6 +460,8 @@ class Submission(ChaHubSaveMixin, models.Model):
is_public = models.BooleanField(default=False)
is_specific_task_re_run = models.BooleanField(default=False)
worker_hostname = models.CharField(max_length=255, blank=True, null=True)
queue = models.ForeignKey('queues.Queue', on_delete=models.SET_NULL, null=True, blank=True,
related_name='submissions')
is_migrated = models.BooleanField(default=False)
created_by_migration = models.ForeignKey(Phase, related_name='migrated_submissions', on_delete=models.CASCADE,
null=True,
Expand Down
2 changes: 2 additions & 0 deletions src/apps/competitions/unpackers/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def __init__(self, *args, **kwargs):
"description": self.competition_yaml.get("description", ""),
"competition_type": self.competition_yaml.get("competition_type", "competition"),
"fact_sheet": self.competition_yaml.get("fact_sheet", None),
"reward": self.competition_yaml.get("reward", None),
"contact_email": self.competition_yaml.get("contact_email", None),
"pages": [],
"phases": [],
"leaderboards": [],
Expand Down
24 changes: 18 additions & 6 deletions src/apps/pages/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from datetime import timedelta

from django.http import HttpResponse
from django.utils.timezone import now
from django.views.generic import TemplateView
from django.db.models import Count, Q
Expand Down Expand Up @@ -59,15 +57,29 @@ class ServerStatusView(TemplateView):
template_name = 'pages/server_status.html'

def get_context_data(self, *args, **kwargs):
if not self.request.user.is_staff:
raise HttpResponse(status=404)

show_child_submissions = self.request.GET.get('show_child_submissions', False)

# Get all submissions
qs = Submission.objects.all()

# If user is not super user then:
# filter this user's own submissions
# and
# submissions running on queue which belongs to this user
if not self.request.user.is_superuser:
qs = qs.filter(
Q(owner=self.request.user) |
Q(phase__competition__queue__isnull=False, phase__competition__queue__owner=self.request.user)
)

# filter for fetching last 2 days submissions
qs = qs.filter(created_when__gte=now() - timedelta(days=2))

# filter out child submissions i.e. submission has no parent
if not show_child_submissions:
qs = qs.filter(parent__isnull=True)

qs = qs.order_by('-created_when')
qs = qs.select_related('phase__competition', 'owner')

Expand All @@ -78,8 +90,8 @@ def get_context_data(self, *args, **kwargs):
for submission in context['submissions']:
# Get filesize from each submissions's data
submission.file_size = self.format_file_size(submission.data.file_size)
# Get queue from each submission's competition
queue_name = "*" if submission.phase.competition.queue is None else submission.phase.competition.queue.name
# Get queue from each submission
queue_name = "*" if submission.queue is None else submission.queue.name
submission.competition_queue = queue_name

return context
Expand Down
Binary file added src/static/img/trophy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading