Repository: Cloud-CV/EvalAI

Issue:
Add support for exporting all the submissions in csv format



Discussion:
@deshraj I think we will need `Team Name,` `Challenge Phase`,` Status`, `Created By`, `Execution Time`, `Submission Number`, `Submitted At `fields for the csv file. Please tell if we need anything else ?

Resolving patch:
diff --git a/apps/challenges/urls.py b/apps/challenges/urls.py
--- a/apps/challenges/urls.py
+++ b/apps/challenges/urls.py
@@ -31,4 +31,7 @@
     url(r'(?P<challenge_pk>[0-9]+)/challenge_phase/(?P<challenge_phase_pk>[0-9]+)/submissions$',
         views.get_all_submissions_of_challenge,
         name='get_all_submissions_of_challenge'),
+    url(r'(?P<challenge_pk>[0-9]+)/download_all_submissions_file/(?P<file_type>[A-Za-z]+)/$',
+        views.download_all_submissions_file,
+        name='download_all_submissions_file')
 ]
diff --git a/apps/challenges/views.py b/apps/challenges/views.py
--- a/apps/challenges/views.py
+++ b/apps/challenges/views.py
@@ -1,3 +1,4 @@
+import csv
 import logging
 import random
 import requests
@@ -11,6 +12,7 @@
 
 from django.core.files.base import ContentFile
 from django.db import transaction
+from django.http import HttpResponse
 from django.utils import timezone
 
 from rest_framework import permissions, status
@@ -736,3 +738,56 @@ def get_all_submissions_of_challenge(request, challenge_pk, challenge_phase_pk):
     else:
         response_data = {'error': 'You are neither host nor participant of the challenge!'}
         return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
+
+
+@throttle_classes([UserRateThrottle])
+@api_view(['GET'])
+@permission_classes((permissions.IsAuthenticated, HasVerifiedEmail))
+@authentication_classes((ExpiringTokenAuthentication,))
+def download_all_submissions_file(request, challenge_pk, file_type):
+
+    # To check for the corresponding challenge from challenge_pk.
+    challenge = get_challenge_model(challenge_pk)
+
+    if file_type == 'csv':
+        if is_user_a_host_of_challenge(user=request.user, challenge_pk=challenge_pk):
+            submissions = Submission.objects.filter(challenge_phase__challenge=challenge).order_by('-submitted_at')
+            response = HttpResponse(content_type='text/csv')
+            response['Content-Disposition'] = 'attachment; filename=all_submissions.csv'
+            writer = csv.writer(response)
+            writer.writerow(['id',
+                             'Team Name',
+                             'Challenge Phase',
+                             'Status',
+                             'Created By',
+                             'Execution Time(sec.)',
+                             'Submission Number',
+                             'Submitted File',
+                             'Stdout File',
+                             'Stderr File',
+                             'Submitted At',
+                             'Submission Result File',
+                             'Submission Metadata File',
+                             ])
+            for submission in submissions:
+                writer.writerow([submission.id,
+                                 submission.participant_team,
+                                 submission.challenge_phase,
+                                 submission.status,
+                                 submission.created_by,
+                                 submission.execution_time,
+                                 submission.submission_number,
+                                 submission.input_file,
+                                 submission.stdout_file,
+                                 submission.stderr_file,
+                                 submission.created_at,
+                                 submission.submission_result_file,
+                                 submission.submission_metadata_file,
+                                 ])
+            return response
+        else:
+            response_data = {'error': 'Only Challenge Hosts can use the export feature!'}
+            return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
+    else:
+        response_data = {'error': 'The file type requested is not valid!'}
+        return Response(response_data, status=status.HTTP_400_BAD_REQUEST)


Test patch:
diff --git a/tests/unit/challenges/test_urls.py b/tests/unit/challenges/test_urls.py
--- a/tests/unit/challenges/test_urls.py
+++ b/tests/unit/challenges/test_urls.py
@@ -2,7 +2,7 @@
 
 from django.contrib.auth.models import User
 from django.core.files.uploadedfile import SimpleUploadedFile
-from django.urls import reverse_lazy
+from django.urls import reverse_lazy, resolve
 from django.utils import timezone
 
 from rest_framework.test import APITestCase, APIClient
@@ -55,6 +55,8 @@ def setUp(self):
             team_name='Participant Team for Challenge',
             created_by=self.user)
 
+        self.file_type = 'csv'
+
         self.client.force_authenticate(user=self.user)
 
 
@@ -92,3 +94,11 @@ def test_challenges_urls(self):
 
         url = reverse_lazy('challenges:get_all_challenges', kwargs={'challenge_time': "PAST"})
         self.assertEqual(url, '/api/challenges/challenge/PAST')
+
+        self.url = reverse_lazy('challenges:download_all_submissions_file',
+                                kwargs={'challenge_pk': self.challenge.pk,
+                                        'file_type': self.file_type})
+        self.assertEqual(self.url, '/api/challenges/{}/download_all_submissions_file/{}/'.format(self.challenge.pk,
+                                                                                                 self.file_type))
+        resolver = resolve(self.url)
+        self.assertEqual(resolver.view_name, 'challenges:download_all_submissions_file')
diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py
--- a/tests/unit/challenges/test_views.py
+++ b/tests/unit/challenges/test_views.py
@@ -1754,3 +1754,148 @@ def test_get_all_submissions_when_user_is_neither_host_nor_participant_of_challe
         response = self.client.get(self.url, {})
         self.assertEqual(response.data, expected)
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+class DownloadAllSubmissionsFileTest(BaseAPITestClass):
+
+    def setUp(self):
+        super(DownloadAllSubmissionsFileTest, self).setUp()
+
+        self.user8 = User.objects.create(
+            username='otheruser8',
+            password='other_secret_password',
+            email='user8@test.com',)
+
+        self.user9 = User.objects.create(
+            username='otheruser9',
+            password='other_secret_password',
+            email='user9@test.com',)
+
+        EmailAddress.objects.create(
+            user=self.user8,
+            email='user8@test.com',
+            primary=True,
+            verified=True)
+
+        EmailAddress.objects.create(
+            user=self.user9,
+            email='user9@test.com',
+            primary=True,
+            verified=True)
+
+        self.challenge_host_team8 = ChallengeHostTeam.objects.create(
+            team_name='Other Test Challenge Host Team',
+            created_by=self.user8
+        )
+
+        # Now allot self.user as also a host of self.challenge_host_team1
+        self.challenge_host8 = ChallengeHost.objects.create(
+            user=self.user8,
+            team_name=self.challenge_host_team8,
+            status=ChallengeHost.ACCEPTED,
+            permissions=ChallengeHost.ADMIN
+        )
+
+        self.participant_team8 = ParticipantTeam.objects.create(
+            team_name='Participant Team for Challenge8',
+            created_by=self.user8)
+
+        self.participant8 = Participant.objects.create(
+            user=self.user8,
+            status=Participant.ACCEPTED,
+            team=self.participant_team)
+
+        self.challenge8 = Challenge.objects.create(
+            title='Other Test Challenge',
+            short_description='Short description for other test challenge',
+            description='Description for other test challenge',
+            terms_and_conditions='Terms and conditions for other test challenge',
+            submission_guidelines='Submission guidelines for other test challenge',
+            creator=self.challenge_host_team8,
+            published=False,
+            enable_forum=True,
+            anonymous_leaderboard=False,
+            start_date=timezone.now() - timedelta(days=2),
+            end_date=timezone.now() + timedelta(days=1),
+        )
+
+        try:
+            os.makedirs('/tmp/evalai')
+        except OSError:
+            pass
+
+        with self.settings(MEDIA_ROOT='/tmp/evalai'):
+            self.challenge_phase8 = ChallengePhase.objects.create(
+                name='Challenge Phase',
+                description='Description for Challenge Phase',
+                leaderboard_public=False,
+                is_public=True,
+                start_date=timezone.now() - timedelta(days=2),
+                end_date=timezone.now() + timedelta(days=1),
+                challenge=self.challenge8,
+                test_annotation=SimpleUploadedFile('test_sample_file.txt',
+                                                   'Dummy file content', content_type='text/plain')
+            )
+        with self.settings(MEDIA_ROOT='/tmp/evalai'):
+            self.submission8 = Submission.objects.create(
+                participant_team=self.participant_team8,
+                challenge_phase=self.challenge_phase8,
+                created_by=self.challenge_host_team8.created_by,
+                status='submitted',
+                input_file=SimpleUploadedFile('test_sample_file.txt',
+                                              'Dummy file content', content_type='text/plain'),
+                method_name="Test Method",
+                method_description="Test Description",
+                project_url="http://testserver/",
+                publication_url="http://testserver/",
+                is_public=True,
+            )
+
+        self.file_type_csv = 'csv'
+
+        self.file_type_pdf = 'pdf'
+
+        self.client.force_authenticate(user=self.user8)
+
+    def test_download_all_submissions_file_when_challenge_does_not_exist(self):
+        self.url = reverse_lazy('challenges:download_all_submissions_file',
+                                kwargs={'challenge_pk': self.challenge8.pk+10,
+                                        'file_type': self.file_type_csv})
+        expected = {
+            'detail': 'Challenge {} does not exist'.format(self.challenge8.pk+10)
+        }
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_download_all_submissions_when_file_type_is_not_csv(self):
+        self.url = reverse_lazy('challenges:download_all_submissions_file',
+                                kwargs={'challenge_pk': self.challenge8.pk,
+                                        'file_type': self.file_type_pdf})
+        expected = {
+            'error': 'The file type requested is not valid!'
+        }
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def test_download_all_submissions(self):
+        self.url = reverse_lazy('challenges:download_all_submissions_file',
+                                kwargs={'challenge_pk': self.challenge8.pk,
+                                        'file_type': self.file_type_csv})
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_download_all_submissions_when_user_is_not_a_challenge_host(self):
+        self.url = reverse_lazy('challenges:download_all_submissions_file',
+                                kwargs={'challenge_pk': self.challenge8.pk,
+                                        'file_type': self.file_type_csv})
+
+        self.client.force_authenticate(user=self.user9)
+
+        expected = {
+            'error': 'Only Challenge Hosts can use the export feature!'
+        }
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)