Skip to content

Commit

Permalink
feat(following): enable registered users to follow and unfollow others
Browse files Browse the repository at this point in the history
- Add Follows model and serializer
- Add views for following, unfollowing and listing follows and followers
- Move tests for profiles app to a tests folder
- Write tests for added functionality

[Finishes #162163174]
  • Loading branch information
Isaac Ongebo authored and Isaac Ongebo committed Dec 18, 2018
1 parent b511faa commit 08ea4b5
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ def setUp(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
user =User.objects.get()
self.assertTrue(user.is_verified)
# self.client =

def account_verification(self, token, uid):
request = APIRequestFactory().get(
Expand Down
2 changes: 1 addition & 1 deletion authors/apps/authentication/tests/test_validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_for_wrong_password(self):
data = {"user": { "username" : "samrub", "email" : "samrub@gmail.com", "password" : "dvhhshbdbbb"}}
response = self.client.post('/api/users/' ,data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self. assertIn("Weak password: password should contain at least 8 characters long ,capital letter and a number", str(response.data))
self. assertIn("Weak password: password should contain at least 8 characters, a capital letter and a number.", str(response.data))

def test_for_wrong_email(self):
"""
Expand Down
4 changes: 2 additions & 2 deletions authors/apps/authentication/validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ def valid_password(password):
{

'password':
'Weak password: password should contain'+
' at least 8 characters long ' + ',' + 'capital letter and a number'
'Weak password: password should contain ' +
'at least 8 characters, a capital letter and a number.'
}

)
Expand Down
8 changes: 7 additions & 1 deletion authors/apps/profiles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ class UserProfile(models.Model):

def __str__(self):
return self.user.username



class Follow(models.Model):
follower = models.ForeignKey(User, on_delete=models.CASCADE)
followed = models.ForeignKey(UserProfile, on_delete=models.CASCADE)


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Expand Down
8 changes: 7 additions & 1 deletion authors/apps/profiles/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import UserProfile
from .models import UserProfile, Follow


class GetUserProfileSerializer(serializers.ModelSerializer):
Expand All @@ -13,3 +13,9 @@ class UpdateProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ['photo','bio','fun_fact']


class FollowSerializer(serializers.ModelSerializer):
class Meta:
model = Follow
fields = ('follower', 'followed')
Empty file.
96 changes: 96 additions & 0 deletions authors/apps/profiles/tests/test_follow_users_feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from rest_framework.test import APITestCase, APIClient, APIRequestFactory
from authors.apps.authentication.models import User
from authors.apps.authentication.views import RegistrationAPIView, AccountVerified
from django.urls import reverse
from rest_framework import status


class FollowUsersTests(APITestCase):
def setUp(self):
self.user1 = {'user': {
'username': 'Jack Sparrow',
'email': 'jacksparrow@gmail.com',
'password': 'J4ckSparrow'
}}
self.user2 = {'user': {
'username': 'Thor',
'email': 'ragnarok@thor.com',
'password': '4Sgardian'
}}
self.client.post('/api/users/', data=self.user1, format='json')
self.verify_account(self.user1['user']['username'])
self.client.post('/api/users/', data=self.user2, format='json')
self.verify_account(self.user2['user']['username'])
response = self.client.post('/api/users/login/', self.user1, format='json')
self.auth_header_1 = 'Bearer {}'.format(response.data['token'])
response = self.client.post('/api/users/login/', self.user2, format='json')
self.auth_header_2 = 'Bearer {}'.format(response.data['token'])

def verify_account(self, username):
user = User.objects.get(username=username)
request = APIRequestFactory().post(reverse('registration'))
token, uid = RegistrationAPIView.generate_token(user, request)
response = APIRequestFactory().get(
reverse('verify_account', kwargs={'token': token, 'uid': uid})
)
verify = AccountVerified.as_view()
response = verify(response, token=token, uid=uid)
self.assertEqual(response.status_code, 200)

def test_user_can_follow_another_user(self):
self.client.credentials(HTTP_AUTHORIZATION=self.auth_header_1)
response = self.client.post(
'/api/profiles/{}/follows/'.format(self.user2['user']['username']), format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(response.data['following'])

def test_user_cannot_follow_own_self_or_another_user_twice(self):
self.client.credentials(HTTP_AUTHORIZATION=self.auth_header_2)
response = self.client.post(
'/api/profiles/{}/follows/'.format(self.user2['user']['username']), format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue(response.data['error'])
response = self.client.post(
'/api/profiles/{}/follows/'.format(self.user1['user']['username']), format='json'
)
response = self.client.post(
'/api/profiles/{}/follows/'.format(self.user1['user']['username']), format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue(response.data['error'])

def test_user_can_unfollow_another_user(self):
self.client.credentials(HTTP_AUTHORIZATION=self.auth_header_1)
response = self.client.post(
'/api/profiles/{}/follows/'.format(self.user2['user']['username']), format='json'
)
response = self.client.delete(
'/api/profiles/{}/follows/'.format(self.user2['user']['username']), format='json'
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

def test_user_cannot_unfollow_a_user_they_are_not_following(self):
self.client.credentials(HTTP_AUTHORIZATION=self.auth_header_1)
response = self.client.delete(
'/api/profiles/{}/follows/'.format(self.user1['user']['username']), format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue(response.data['error'])

def test_api_can_return_list_of_followings_for_a_user(self):
self.client.credentials(HTTP_AUTHORIZATION=self.auth_header_2)
response = self.client.get(
'/api/profiles/{}/follows/'.format(self.user1['user']['username'], format='json')
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue('following' in response.data)

def test_api_can_return_list_of_followers_for_a_user(self):
self.client.credentials(HTTP_AUTHORIZATION=self.auth_header_1)
response = self.client.get(
'/api/profiles/{}/followers/'.format(self.user2['user']['username'], format='json')
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue('followers' in response.data)
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from rest_framework import status
from django.urls import path, reverse
from rest_framework.test import APITestCase, APIRequestFactory, APIClient
from ..authentication.views import RegistrationAPIView, AccountVerified
from ..authentication.models import User
from ..authentication.backends import JWTAuthentication
from ...authentication.views import RegistrationAPIView, AccountVerified
from ...authentication.models import User
from ...authentication.backends import JWTAuthentication


class TestUserProfile(APITestCase):
Expand Down
7 changes: 4 additions & 3 deletions authors/apps/profiles/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django.urls import path

from .views import UserProfiles,Updateprofile
from .views import UserProfiles, Updateprofile, FollowsView, FollowersView

urlpatterns = [
path('users/profiles', UserProfiles.as_view()),
path('users/profiles/', Updateprofile.as_view(),name="profile")

path('users/profiles/', Updateprofile.as_view(),name="profile"),
path('profiles/<username>/follows/', FollowsView.as_view()),
path('profiles/<username>/followers/', FollowersView.as_view()),
]
100 changes: 97 additions & 3 deletions authors/apps/profiles/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from django.shortcuts import render
from .serializers import GetUserProfileSerializer, UpdateProfileSerializer
from .serializers import (
GetUserProfileSerializer, UpdateProfileSerializer, FollowSerializer
)
from rest_framework import generics, viewsets
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from rest_framework import status
from .models import UserProfile
from .models import UserProfile, Follow
from authors.apps.authentication.models import User
from rest_framework.exceptions import NotFound


class UserProfiles(generics.ListAPIView):
Expand All @@ -23,4 +27,94 @@ def update(self,request):
serializer =self.serializer_class(request.user.userprofile, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.update(request.user.userprofile, request.data)
return Response(serializer.data)
return Response(serializer.data)


class FollowsView(APIView):
permission_classes = (IsAuthenticated,)

def post(self, request, username):
follower_id = User.objects.get(username=request.user.username).id
try:
followed_id = User.objects.get(username=username).id
self.profile_id = UserProfile.objects.get(user_id=followed_id).id
self.verify_following_criteria_met(follower_id, followed_id, username)
except Exception as e:
if isinstance(e, User.DoesNotExist):
raise NotFound('No user with name {} exists.'.format(username))
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
follow_data = {'follower': follower_id, 'followed': self.profile_id}
serializer = FollowSerializer(data=follow_data)
serializer.is_valid(raise_exception=True)
serializer.save()
profile = self.get_followed_profile(self.profile_id)
return Response(profile, status=status.HTTP_201_CREATED)

def verify_following_criteria_met(self, follower_id, followed_id, name):
if follower_id == followed_id:
raise Exception('You cannot follow your own profile.')
query_result = Follow.objects.filter(follower_id=follower_id, followed_id=self.profile_id)
if len(query_result) != 0:
raise Exception('Already following {}.'.format(name))

def get_followed_profile(self, followed):
profile = UserProfile.objects.get(id=followed)
serializer = GetUserProfileSerializer(profile)
profile = serializer.data
profile['following'] = True
return profile

def delete(self, request, username):
user_id = User.objects.get(username=request.user.username).id
try:
followed_id = User.objects.get(username=username).id
profile_id = UserProfile.objects.get(user_id=followed_id).id
follow = Follow.objects.filter(follower_id=user_id, followed_id=profile_id)
if len(follow) == 0:
raise Exception('Cannot unfollow a user you are not following.')
follow.delete()
return Response(
{'message': 'Successfully unfollowed {}.'.format(username)},
status=status.HTTP_204_NO_CONTENT
)
except Exception as e:
if isinstance(e, User.DoesNotExist):
return Response(
{'error': 'No user with name {} exists.'.format(username)},
status=status.HTTP_400_BAD_REQUEST
)
return Response(
{'error': str(e)}, status=status.HTTP_400_BAD_REQUEST
)

def get(self, request, username):
try:
user_id = User.objects.get(username=username).id
except:
raise NotFound('No user with name {} exists.'.format(username))
follows = Follow.objects.filter(follower_id=user_id)
serializer = FollowSerializer(follows, many=True)
following = list()
for follow in serializer.data:
user_id = UserProfile.objects.get(id=follow['followed']).user_id
username = User.objects.get(id=user_id).username
following.append(username)
return Response({'following': following}, status=status.HTTP_200_OK)


class FollowersView(APIView):
permission_classes = (IsAuthenticated,)

def get(self, request, username):
try:
user_id = User.objects.get(username=username).id
except:
raise NotFound('No user with name {} exists.'.format(username))
profile_id = UserProfile.objects.get(user_id=user_id).id
followers = Follow.objects.filter(followed_id=profile_id)
serializer = FollowSerializer(followers, many=True)
followers = list()
for follow in serializer.data:
username = User.objects.get(id=follow['follower']).username
followers.append(username)
return Response({'followers': followers}, status=status.HTTP_200_OK)

0 comments on commit 08ea4b5

Please sign in to comment.