Repository: Cloud-CV/EvalAI

Issue:
Create an API Endpoint to show all the submissions to Challenge Host for a particular Challenge
### Objective

The aim is to create an API endpoint which fetches all the submissions of a Challenge and and returns as a JSON Response. 

### Constraints:
- URL: `/api/challenges/<challenge_pk>/submissions/`
- Only the Challenge Host of that particular challenge can access this information. 
- Request Method: `GET`
- Response on success: `200`
- Response on not allowed: `406`
- Pagination: `YES`


### Stretch Goal
After we have the API ready, we would like to add support for filtering the submissions according to the `date of submission`, `challenge phase`, `status of submissions`. 


Discussion:
@deshraj can I work on this one?
@deshraj can I work on this?
@yadavankit Yeah, you can work on this. :) 
@yadavankit Please don't start working on this if you haven't started yet. I would like to do it myself since we need this feature ASAP. Sorry for the last minute information. 
@deshraj I saw this just now, had to run some errands. No issues :)
Cool, thanks for coordinating. :) 

Resolving patch:
diff --git a/apps/challenges/urls.py b/apps/challenges/urls.py
--- a/apps/challenges/urls.py
+++ b/apps/challenges/urls.py
@@ -25,4 +25,7 @@
         name='get_challenges_based_on_teams'),
     url(r'(?P<challenge_pk>[0-9]+)/challenge_phase_split$', views.challenge_phase_split_list,
         name='challenge_phase_split_list'),
+    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'),
 ]
diff --git a/apps/challenges/views.py b/apps/challenges/views.py
--- a/apps/challenges/views.py
+++ b/apps/challenges/views.py
@@ -11,11 +11,15 @@
 
 from accounts.permissions import HasVerifiedEmail
 from base.utils import paginated_queryset
+from challenges.utils import get_challenge_model
 from hosts.models import ChallengeHost, ChallengeHostTeam
-from hosts.utils import get_challenge_host_teams_for_user
+from hosts.utils import get_challenge_host_teams_for_user, is_user_a_host_of_challenge
+from jobs.models import Submission
+from jobs.serializers import SubmissionSerializer, ChallengeSubmissionManagementSerializer
 from participants.models import Participant, ParticipantTeam
-from participants.utils import get_participant_teams_for_user, has_user_participated_in_challenge
-
+from participants.utils import (get_participant_teams_for_user,
+                                has_user_participated_in_challenge,
+                                get_participant_team_id_of_user_for_a_challenge,)
 
 from .models import Challenge, ChallengePhase, ChallengePhaseSplit
 from .permissions import IsChallengeCreator
@@ -340,3 +344,60 @@ def challenge_phase_split_list(request, challenge_pk):
     serializer = ChallengePhaseSplitSerializer(result_page, many=True)
     response_data = serializer.data
     return paginator.get_paginated_response(response_data)
+
+
+@throttle_classes([UserRateThrottle])
+@api_view(['GET'])
+@permission_classes((permissions.IsAuthenticated, HasVerifiedEmail))
+@authentication_classes((ExpiringTokenAuthentication,))
+def get_all_submissions_of_challenge(request, challenge_pk, challenge_phase_pk):
+    """
+    Returns all the submissions for a particular challenge
+    """
+    # To check for the corresponding challenge from challenge_pk.
+    challenge = get_challenge_model(challenge_pk)
+
+    # To check for the corresponding challenge phase from the challenge_phase_pk and challenge.
+    try:
+        challenge_phase = ChallengePhase.objects.get(pk=challenge_phase_pk, challenge=challenge)
+    except ChallengePhase.DoesNotExist:
+        response_data = {'error': 'Challenge Phase {} does not exist'.format(challenge_phase_pk)}
+        return Response(response_data, status=status.HTTP_404_NOT_FOUND)
+
+    # To check for the user as a host of the challenge from the request and challenge_pk.
+    if is_user_a_host_of_challenge(user=request.user, challenge_pk=challenge_pk):
+
+        # Filter submissions on the basis of challenge for host for now. Later on, the support for query
+        # parameters like challenge phase, date is to be added.
+
+        submissions = Submission.objects.filter(challenge_phase__challenge=challenge).order_by('-submitted_at')
+        paginator, result_page = paginated_queryset(submissions, request)
+        try:
+            serializer = ChallengeSubmissionManagementSerializer(result_page, many=True, context={'request': request})
+            response_data = serializer.data
+            return paginator.get_paginated_response(response_data)
+        except:
+            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+    # To check for the user as a participant of the challenge from the request and challenge_pk.
+    elif has_user_participated_in_challenge(user=request.user, challenge_id=challenge_pk):
+
+        # get participant team object for the user for a particular challenge.
+        participant_team_pk = get_participant_team_id_of_user_for_a_challenge(
+                request.user, challenge_pk)
+
+        # Filter submissions on the basis of challenge phase for a participant.
+        submissions = Submission.objects.filter(participant_team=participant_team_pk,
+                                                challenge_phase=challenge_phase).order_by('-submitted_at')
+        paginator, result_page = paginated_queryset(submissions, request)
+        try:
+            serializer = SubmissionSerializer(result_page, many=True, context={'request': request})
+            response_data = serializer.data
+            return paginator.get_paginated_response(response_data)
+        except:
+            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+    # when user is neither host not participant of the challenge.
+    else:
+        response_data = {'error': 'You are neither host nor participant of the challenge!'}
+        return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
diff --git a/apps/jobs/serializers.py b/apps/jobs/serializers.py
--- a/apps/jobs/serializers.py
+++ b/apps/jobs/serializers.py
@@ -91,4 +91,4 @@ def get_participant_team_members_email_ids(self, obj):
             return 'Participant team does not exist'
 
         participant_ids = Participant.objects.filter(team=participant_team).values_list('user_id', flat=True)
-        return User.objects.filter(id__in=participant_ids).values_list('email', flat=True)
+        return list(User.objects.filter(id__in=participant_ids).values_list('email', flat=True))


Test patch:
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
@@ -15,8 +15,9 @@
 from rest_framework.test import APITestCase, APIClient
 
 from challenges.models import Challenge, ChallengePhase, DatasetSplit, ChallengePhaseSplit, Leaderboard
-from participants.models import Participant, ParticipantTeam
 from hosts.models import ChallengeHost, ChallengeHostTeam
+from jobs.models import Submission
+from participants.models import Participant, ParticipantTeam
 
 
 class BaseAPITestClass(APITestCase):
@@ -1395,3 +1396,211 @@ def test_challenge_phase_split_when_challenge_phase_does_not_exist(self):
         response = self.client.get(self.url, {})
         self.assertEqual(response.data, expected)
         self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE)
+
+
+class GetAllSubmissionsTest(BaseAPITestClass):
+
+    def setUp(self):
+        super(GetAllSubmissionsTest, self).setUp()
+
+        self.user5 = User.objects.create(
+            username='otheruser',
+            password='other_secret_password',
+            email='user5@test.com',)
+
+        self.user6 = User.objects.create(
+            username='not a challenge host',
+            password='secret password',
+            email='user6@test.com',)
+
+        self.user7 = User.objects.create(
+            username='not a challenge host of challenge5',
+            password='secret password')
+
+        EmailAddress.objects.create(
+            user=self.user5,
+            email='user5@test.com',
+            primary=True,
+            verified=True)
+
+        EmailAddress.objects.create(
+            user=self.user6,
+            email='user6@test.com',
+            primary=True,
+            verified=True)
+
+        EmailAddress.objects.create(
+            user=self.user7,
+            email='user7@test.com',
+            primary=True,
+            verified=True)
+
+        self.challenge_host_team7 = ChallengeHostTeam.objects.create(
+            team_name='Other Test Challenge Host Team7',
+            created_by=self.user7
+        )
+        self.challenge_host_team5 = ChallengeHostTeam.objects.create(
+            team_name='Other Test Challenge Host Team5',
+            created_by=self.user5
+        )
+
+        # Now allot self.user as also a host of self.challenge_host_team1
+        self.challenge_host5 = ChallengeHost.objects.create(
+            user=self.user5,
+            team_name=self.challenge_host_team5,
+            status=ChallengeHost.ACCEPTED,
+            permissions=ChallengeHost.ADMIN
+        )
+
+        self.participant_team6 = ParticipantTeam.objects.create(
+            team_name='Participant Team for Challenge5',
+            created_by=self.user6)
+
+        self.participant6 = Participant.objects.create(
+            user=self.user6,
+            status=Participant.ACCEPTED,
+            team=self.participant_team6)
+
+        self.challenge5 = 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_team5,
+            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_phase5 = 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.challenge5,
+                test_annotation=SimpleUploadedFile('test_sample_file.txt',
+                                                   'Dummy file content', content_type='text/plain')
+            )
+        with self.settings(MEDIA_ROOT='/tmp/evalai'):
+            self.submission = Submission.objects.create(
+                participant_team=self.participant_team6,
+                challenge_phase=self.challenge_phase5,
+                created_by=self.challenge_host_team5.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.client.force_authenticate(user=self.user6)
+
+    def test_get_all_submissions_when_challenge_does_not_exist(self):
+        self.url = reverse_lazy('challenges:get_all_submissions_of_challenge',
+                                kwargs={'challenge_pk': self.challenge5.pk+10,
+                                        'challenge_phase_pk': self.challenge_phase5.pk})
+        expected = {
+            'detail': 'Challenge {} does not exist'.format(self.challenge5.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_get_all_submissions_when_challenge_phase_does_not_exist(self):
+        self.url = reverse_lazy('challenges:get_all_submissions_of_challenge',
+                                kwargs={'challenge_pk': self.challenge5.pk,
+                                        'challenge_phase_pk': self.challenge_phase5.pk+10})
+        expected = {
+            'error': 'Challenge Phase {} does not exist'.format(self.challenge_phase5.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_get_all_submissions_when_user_is_host_of_challenge(self):
+        self.url = reverse_lazy('challenges:get_all_submissions_of_challenge',
+                                kwargs={'challenge_pk': self.challenge5.pk,
+                                        'challenge_phase_pk': self.challenge_phase5.pk})
+        self.client.force_authenticate(user=self.user5)
+
+        expected = [
+            {
+                "id": self.submission.id,
+                "participant_team": self.submission.participant_team.team_name,
+                "challenge_phase": self.submission.challenge_phase.name,
+                "created_by": self.submission.created_by.username,
+                "status": self.submission.status,
+                "is_public": self.submission.is_public,
+                "submission_number": self.submission.submission_number,
+                "submitted_at": "{0}{1}".format(self.submission.submitted_at.isoformat(), 'Z').replace("+00:00", ""),
+                'execution_time': self.submission.execution_time,
+                "input_file": "http://testserver%s" % (self.submission.input_file.url),
+                "stdout_file": None,
+                "stderr_file": None,
+                "submission_result_file": None,
+                "submission_metadata_file": None,
+                "participant_team_members_email_ids": ['user6@test.com'],
+            }
+        ]
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data['results'], expected)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_get_all_submissions_when_user_is_participant_of_challenge(self):
+        self.url = reverse_lazy('challenges:get_all_submissions_of_challenge',
+                                kwargs={'challenge_pk': self.challenge5.pk,
+                                        'challenge_phase_pk': self.challenge_phase5.pk})
+        self.client.force_authenticate(user=self.user6)
+        expected = [
+            {
+                'id': self.submission.id,
+                'participant_team': self.submission.participant_team.pk,
+                'participant_team_name': self.submission.participant_team.team_name,
+                'execution_time': self.submission.execution_time,
+                'challenge_phase': self.submission.challenge_phase.pk,
+                'created_by': self.submission.created_by.pk,
+                'status': self.submission.status,
+                'input_file': "http://testserver%s" % (self.submission.input_file.url),
+                'method_name': self.submission.method_name,
+                'method_description': self.submission.method_description,
+                'project_url': self.submission.project_url,
+                'publication_url': self.submission.publication_url,
+                'stdout_file': None,
+                'stderr_file': None,
+                'submission_result_file': None,
+                "submitted_at": "{0}{1}".format(self.submission.submitted_at.isoformat(), 'Z').replace("+00:00", ""),
+                "is_public": self.submission.is_public,
+            }
+        ]
+        self.challenge5.participant_teams.add(self.participant_team6)
+        self.challenge5.save()
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data['results'], expected)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_get_all_submissions_when_user_is_neither_host_nor_participant_of_challenge(self):
+        self.client.force_authenticate(user=self.user7)
+        self.url = reverse_lazy('challenges:get_all_submissions_of_challenge',
+                                kwargs={'challenge_pk': self.challenge5.pk,
+                                        'challenge_phase_pk': self.challenge_phase5.pk})
+        expected = {
+            'error': 'You are neither host nor participant of the challenge!'
+        }
+        self.challenge5.participant_teams.add(self.participant_team6)
+        self.challenge5.save()
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)