Repository: Cloud-CV/EvalAI

Issue:
Add API to star a challenge.




Discussion:

Resolving patch:
diff --git a/apps/challenges/serializers.py b/apps/challenges/serializers.py
--- a/apps/challenges/serializers.py
+++ b/apps/challenges/serializers.py
@@ -7,7 +7,8 @@
                      ChallengePhase,
                      ChallengePhaseSplit,
                      DatasetSplit,
-                     Leaderboard,)
+                     Leaderboard,
+                     StarChallenge)
 
 
 class ChallengeSerializer(serializers.ModelSerializer):
@@ -151,3 +152,30 @@ class Meta:
         fields = ('id', 'name', 'description', 'leaderboard_public', 'start_date',
                   'end_date', 'challenge', 'max_submissions_per_day', 'max_submissions',
                   'is_public', 'is_active', 'codename', 'test_annotation')
+
+
+class StarChallengeSerializer(serializers.ModelSerializer):
+
+    count = serializers.SerializerMethodField()
+
+    def __init__(self, *args, **kwargs):
+        super(StarChallengeSerializer, self).__init__(*args, **kwargs)
+        context = kwargs.get('context')
+        if context:
+            challenge = context.get('challenge')
+            if challenge:
+                kwargs['data']['challenge'] = challenge.pk
+            request = context.get('request')
+            if request:
+                kwargs['data']['user'] = request.user.pk
+            starred = context.get('is_starred')
+            if starred:
+                kwargs['data']['is_starred'] = starred
+
+    class Meta:
+        model = StarChallenge
+        fields = ('user', 'challenge', 'count', 'is_starred')
+
+    def get_count(self, obj):
+        count = StarChallenge.objects.filter(challenge=obj.challenge, is_starred=True).count()
+        return count
diff --git a/apps/challenges/urls.py b/apps/challenges/urls.py
--- a/apps/challenges/urls.py
+++ b/apps/challenges/urls.py
@@ -34,5 +34,8 @@
     url(r'(?P<challenge_pk>[0-9]+)/phase/(?P<challenge_phase_pk>[0-9]+)'
         r'/download_all_submissions/(?P<file_type>[A-Za-z]+)/$',
         views.download_all_submissions,
-        name='download_all_submissions')
+        name='download_all_submissions'),
+    url(r'(?P<challenge_pk>[0-9]+)/$',
+        views.star_challenge,
+        name='star_challenge'),
 ]
diff --git a/apps/challenges/views.py b/apps/challenges/views.py
--- a/apps/challenges/views.py
+++ b/apps/challenges/views.py
@@ -36,7 +36,7 @@
                                 has_user_participated_in_challenge,
                                 get_participant_team_id_of_user_for_a_challenge,)
 
-from .models import Challenge, ChallengePhase, ChallengePhaseSplit, ChallengeConfiguration
+from .models import Challenge, ChallengePhase, ChallengePhaseSplit, ChallengeConfiguration, StarChallenge
 from .permissions import IsChallengeCreator
 from .serializers import (ChallengeConfigSerializer,
                           ChallengePhaseSerializer,
@@ -45,6 +45,7 @@
                           ChallengeSerializer,
                           DatasetSplitSerializer,
                           LeaderboardSerializer,
+                          StarChallengeSerializer,
                           ZipChallengeSerializer,
                           ZipChallengePhaseSplitSerializer,)
 from .utils import get_file_content
@@ -840,3 +841,48 @@ def download_all_submissions(request, challenge_pk, challenge_phase_pk, file_typ
     else:
         response_data = {'error': 'The file type requested is not valid!'}
         return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
+
+
+@throttle_classes([UserRateThrottle])
+@api_view(['GET', 'POST'])
+@permission_classes((permissions.IsAuthenticated, HasVerifiedEmail))
+@authentication_classes((ExpiringTokenAuthentication,))
+def star_challenge(request, challenge_pk):
+    """
+    API endpoint for starring and unstarring
+    a challenge.
+    """
+    challenge = get_challenge_model(challenge_pk)
+
+    if request.method == 'POST':
+        try:
+            starred_challenge = StarChallenge.objects.get(user=request.user,
+                                                          challenge=challenge)
+            starred_challenge.is_starred = not starred_challenge.is_starred
+            starred_challenge.save()
+            serializer = StarChallengeSerializer(starred_challenge)
+            response_data = serializer.data
+            return Response(response_data, status=status.HTTP_200_OK)
+        except:
+            serializer = StarChallengeSerializer(data=request.data, context={'request': request,
+                                                                             'challenge': challenge,
+                                                                             'is_starred': True})
+            if serializer.is_valid():
+                serializer.save()
+                response_data = serializer.data
+                return Response(response_data, status=status.HTTP_201_CREATED)
+            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+    if request.method == 'GET':
+        try:
+            starred_challenge = StarChallenge.objects.get(user=request.user,
+                                                          challenge=challenge)
+            serializer = StarChallengeSerializer(starred_challenge)
+            response_data = serializer.data
+            return Response(response_data, status=status.HTTP_200_OK)
+        except:
+            starred_challenge = StarChallenge.objects.filter(challenge=challenge)
+            serializer = StarChallengeSerializer(starred_challenge, many=True)
+            response_data = {'is_starred': False,
+                             'count': serializer.data[0]['count']}
+            return Response(response_data, status=status.HTTP_200_OK)


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
@@ -104,3 +104,11 @@ def test_challenges_urls(self):
                          .format(self.challenge.pk, self.challenge_phase.pk, self.file_type))
         resolver = resolve(self.url)
         self.assertEqual(resolver.view_name, 'challenges:download_all_submissions')
+
+        self.url = reverse_lazy('challenges:star_challenge',
+                                kwargs={'challenge_pk': self.challenge.pk})
+        self.assertEqual(self.url,
+                         '/api/challenges/{}/'
+                         .format(self.challenge.pk))
+        resolver = resolve(self.url)
+        self.assertEqual(resolver.view_name, 'challenges:star_challenge')
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
@@ -21,7 +21,8 @@
                                ChallengePhase,
                                ChallengePhaseSplit,
                                DatasetSplit,
-                               Leaderboard,)
+                               Leaderboard,
+                               StarChallenge)
 from participants.models import Participant, ParticipantTeam
 from hosts.models import ChallengeHost, ChallengeHostTeam
 from jobs.models import Submission
@@ -1895,3 +1896,89 @@ def test_download_all_submissions_when_user_is_neither_a_challenge_host_nor_a_pa
         response = self.client.get(self.url, {})
         self.assertEqual(response.data, expected)
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+class StarChallengesTest(BaseAPITestClass):
+    def setUp(self):
+        super(StarChallengesTest, self).setUp()
+        self.url = reverse_lazy('challenges:star_challenge',
+                                kwargs={'challenge_pk': self.challenge.pk})
+        self.user2 = User.objects.create(
+            username='someuser2',
+            email="user2@test.com",
+            password='secret_password')
+
+        EmailAddress.objects.create(
+            user=self.user2,
+            email='user2@test.com',
+            primary=True,
+            verified=True)
+
+        self.star_challenge = StarChallenge.objects.create(user=self.user,
+                                                           challenge=self.challenge,
+                                                           is_starred=True)
+        self.client.force_authenticate(user=self.user)
+
+    def test_star_challenge_when_challenge_does_not_exist(self):
+        self.url = reverse_lazy('challenges:star_challenge',
+                                kwargs={'challenge_pk': self.challenge.pk+10})
+
+        expected = {
+            'detail': 'Challenge {} does not exist'.format(self.challenge.pk+10)
+        }
+        response = self.client.post(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_get_challenge_when_user_has_starred(self):
+        self.url = reverse_lazy('challenges:star_challenge',
+                                kwargs={'challenge_pk': self.challenge.pk})
+        expected = {
+            'user': self.user.pk,
+            'challenge': self.challenge.pk,
+            'count': 1,
+            'is_starred': True,
+        }
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_get_challenge_when_user_hasnt_starred(self):
+        self.url = reverse_lazy('challenges:star_challenge',
+                                kwargs={'challenge_pk': self.challenge.pk})
+        self.client.force_authenticate(user=self.user2)
+        expected = {
+            'is_starred': False,
+            'count': 1,
+        }
+        response = self.client.get(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_unstar_challenge(self):
+        self.url = reverse_lazy('challenges:star_challenge',
+                                kwargs={'challenge_pk': self.challenge.pk})
+        self.star_challenge.is_starred = False
+        expected = {
+            'user': self.user.pk,
+            'challenge': self.challenge.pk,
+            'count': 0,
+            'is_starred': self.star_challenge.is_starred,
+        }
+        response = self.client.post(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_star_challenge(self):
+        self.url = reverse_lazy('challenges:star_challenge',
+                                kwargs={'challenge_pk': self.challenge.pk})
+        self.star_challenge.delete()
+        expected = {
+            'user': self.user.pk,
+            'challenge': self.challenge.pk,
+            'count': 1,
+            'is_starred': True,
+        }
+        response = self.client.post(self.url, {})
+        self.assertEqual(response.data, expected)
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)