Repository: Cloud-CV/EvalAI

Issue:
Show number of remaining submissions on Submission Page
### USE CASE

Whenever a user tries to make a submission to a particular phase, then we should show these details: 
- Number of submissions he can do for that particular day
- Number of overall remaining submissions he can do in total

### TODO

- [ ] Add support of query params where the keys can be `date`(date in particular format DD-MM-YYYY comes here)
- [ ] On Challenge Submission view, calculate the remaining daily submissions and remaining total submissions using the results of challenge phase API and submission API on the client side and bind it with the view. This will keep getting updated as the user chooses a different phase.
- [ ] Also show the time remaining so that the user knows that when he can make his next submission to that particular phase.

Please let me know if something is unclear. 


Discussion:
@deshraj Can I work on this ?
Sure. 

Resolving patch:
diff --git a/apps/jobs/urls.py b/apps/jobs/urls.py
--- a/apps/jobs/urls.py
+++ b/apps/jobs/urls.py
@@ -9,6 +9,10 @@
     url(r'challenge/(?P<challenge_id>[0-9]+)/'
         r'challenge_phase/(?P<challenge_phase_id>[0-9]+)/submission/',
         views.challenge_submission, name='challenge_submission'),
+    url(r'(?P<challenge_pk>[0-9]+)/'
+        r'phases/(?P<challenge_phase_pk>[0-9]+)/'
+        r'remaining-submissions',
+        views.get_remaining_submissions, name='get_remaining_submissions'),
     url(r'challenge_phase_split/(?P<challenge_phase_split_id>[0-9]+)/leaderboard/',
         views.leaderboard, name='leaderboard'),
 ]
diff --git a/apps/jobs/views.py b/apps/jobs/views.py
--- a/apps/jobs/views.py
+++ b/apps/jobs/views.py
@@ -1,3 +1,5 @@
+import datetime
+
 from rest_framework import permissions, status
 from rest_framework.decorators import (api_view,
                                        authentication_classes,
@@ -6,6 +8,7 @@
 
 from django.db.models.expressions import RawSQL
 from django.db.models import FloatField
+from django.utils import timezone
 
 from rest_framework_expiring_authtoken.authentication import (
     ExpiringTokenAuthentication,)
@@ -19,6 +22,7 @@
     Challenge,
     ChallengePhaseSplit,
     LeaderboardData,)
+from challenges.utils import get_challenge_model, get_challenge_phase_model
 from participants.models import (ParticipantTeam,)
 from participants.utils import (
     get_participant_team_id_of_user_for_a_challenge,)
@@ -228,3 +232,72 @@ def leaderboard(request, challenge_phase_split_id):
     paginator, result_page = paginated_queryset(distinct_sorted_leaderboard_data, request)
     response_data = result_page
     return paginator.get_paginated_response(response_data)
+
+
+@throttle_classes([UserRateThrottle])
+@api_view(['GET'])
+@permission_classes((permissions.IsAuthenticated, HasVerifiedEmail))
+@authentication_classes((ExpiringTokenAuthentication,))
+def get_remaining_submissions(request, challenge_phase_pk, challenge_pk):
+
+    get_challenge_model(challenge_pk)
+
+    challenge_phase = get_challenge_phase_model(challenge_phase_pk)
+
+    participant_team_pk = get_participant_team_id_of_user_for_a_challenge(
+        request.user, challenge_pk)
+
+    # Conditional check for the existence of participant team of the user.
+    if not participant_team_pk:
+        response_data = {'error': 'You haven\'t participated in the challenge'}
+        return Response(response_data, status=status.HTTP_403_FORBIDDEN)
+
+    max_submission_per_day = challenge_phase.max_submissions_per_day
+
+    max_submission = challenge_phase.max_submissions
+
+    submissions_done_today_count = Submission.objects.filter(
+        challenge_phase__challenge=challenge_pk,
+        challenge_phase=challenge_phase_pk,
+        participant_team=participant_team_pk,
+        submitted_at__gte=timezone.now().date()).count()
+
+    failed_submissions_count = Submission.objects.filter(
+        challenge_phase__challenge=challenge_pk,
+        challenge_phase=challenge_phase_pk,
+        participant_team=participant_team_pk,
+        status=Submission.FAILED,
+        submitted_at__gte=timezone.now().date()).count()
+
+    # Checks if today's successfull submission is greater than or equal to max submission per day.
+    if ((submissions_done_today_count - failed_submissions_count) >= max_submission_per_day
+            or (max_submission_per_day == 0)):
+        # Get the UTC time of the instant when the above condition is true.
+        date_time_now = timezone.now()
+        # Calculate the next day's date.
+        date_time_tomorrow = date_time_now.date() + datetime.timedelta(1)
+        utc = timezone.utc
+        # Get the midnight time of the day i.e. 12:00 AM of next day.
+        midnight = utc.localize(datetime.datetime.combine(
+            date_time_tomorrow, datetime.time()))
+        # Subtract the current time from the midnight time to get the remaining time for the next day's submissions.
+        remaining_time = midnight - date_time_now
+        # Return the remaining time with a message.
+        response_data = {'message': 'You have exhausted today\'s submission limit',
+                         'remaining_time': remaining_time
+                         }
+        return Response(response_data, status=status.HTTP_200_OK)
+    else:
+        # Calculate the remaining submissions for today.
+        remaining_submissions_today_count = (max_submission_per_day -
+                                             (submissions_done_today_count -
+                                              failed_submissions_count)
+                                             )
+        # calculate the remaining submissions from total submissions.
+        remaining_submission_count = max_submission - \
+            (submissions_done_today_count - failed_submissions_count)
+        # Return the above calculated data.
+        response_data = {'remaining_submissions_today_count': remaining_submissions_today_count,
+                         'remaining_submissions': remaining_submission_count
+                         }
+        return Response(response_data, status=status.HTTP_200_OK)


Test patch:
diff --git a/tests/unit/jobs/test_views.py b/tests/unit/jobs/test_views.py
--- a/tests/unit/jobs/test_views.py
+++ b/tests/unit/jobs/test_views.py
@@ -371,3 +371,109 @@ def test_get_challenge_submissions(self):
         response = self.client.get(self.url, {})
         self.assertEqual(response.data['results'], expected)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+
+class GetRemainingSubmissionTest(BaseAPITestClass):
+
+    def setUp(self):
+        super(GetRemainingSubmissionTest, self).setUp()
+        self.url = reverse_lazy('jobs:get_remaining_submissions',
+                                kwargs={'challenge_phase_id': self.challenge_phase.pk,
+                                        'challenge_id': self.challenge.pk})
+
+        self.submission1 = Submission.objects.create(
+            participant_team=self.participant_team,
+            challenge_phase=self.challenge_phase,
+            created_by=self.challenge_host_team.created_by,
+            status='submitted',
+            input_file=self.challenge_phase.test_annotation,
+            method_name="Test Method",
+            method_description="Test Description",
+            project_url="http://testserver/",
+            publication_url="http://testserver/",
+            is_public=True,
+        )
+
+        self.submission2 = Submission.objects.create(
+            participant_team=self.participant_team,
+            challenge_phase=self.challenge_phase,
+            created_by=self.challenge_host_team.created_by,
+            status='failed',
+            input_file=self.challenge_phase.test_annotation,
+            method_name="Test Method",
+            method_description="Test Description",
+            project_url="http://testserver/",
+            publication_url="http://testserver/",
+            is_public=True,
+        )
+
+    def test_get_remaining_submission_when_challenge_does_not_exist(self):
+        self.url = reverse_lazy('jobs:get_remaining_submissions',
+                                kwargs={'challenge_phase_pk': self.challenge_phase.pk,
+                                        'challenge_pk': self.challenge.pk+1})
+
+        expected = {
+            'detail': 'Challenge {} does not exist'.format(self.challenge.pk+1)
+        }
+
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_get_remaining_submission_when_challenge_phase_does_not_exist(self):
+        self.url = reverse_lazy('jobs:get_remaining_submissions',
+                                kwargs={'challenge_phase_pk': self.challenge_phase.pk+1,
+                                        'challenge_pk': self.challenge.pk})
+
+        expected = {
+            'detail': 'ChallengePhase {} does not exist'.format(self.challenge_phase.pk+1)
+        }
+
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_get_remaining_submission_when_participant_team_hasnt_participated_in_challenge(self):
+        self.url = reverse_lazy('jobs:get_remaining_submissions',
+                                kwargs={'challenge_phase_pk': self.challenge_phase.pk,
+                                        'challenge_pk': self.challenge.pk})
+
+        expected = {
+            'error': 'You haven\'t participated in the challenge'
+        }
+
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+    def test_get_remaining_submission(self):
+        self.url = reverse_lazy('jobs:get_remaining_submissions',
+                                kwargs={'challenge_phase_pk': self.challenge_phase.pk,
+                                        'challenge_pk': self.challenge.pk})
+        expected = {
+            'remaining_submissions_today_count': 99998,
+            'remaining_submissions': 99998
+        }
+
+        self.challenge.participant_teams.add(self.participant_team)
+        self.challenge.save()
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_get_remaining_submission_time_when_limit_is_exhausted(self):
+        self.url = reverse_lazy('jobs:get_remaining_submissions',
+                                kwargs={'challenge_phase_pk': self.challenge_phase.pk,
+                                        'challenge_pk': self.challenge.pk})
+        setattr(self.challenge_phase, 'max_submissions_per_day', 1)
+        self.challenge_phase.save()
+
+        expected = {
+            'message': 'You have exhausted today\'s submission limit',
+        }
+
+        self.challenge.participant_teams.add(self.participant_team)
+        self.challenge.save()
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data['message'], expected['message'])
+        self.assertEqual(response.status_code, status.HTTP_200_OK)