diff --git a/authors/apps/authentication/tests/test_list_users_functionality.py b/authors/apps/authentication/tests/test_list_users_functionality.py index 40064da..147e45e 100644 --- a/authors/apps/authentication/tests/test_list_users_functionality.py +++ b/authors/apps/authentication/tests/test_list_users_functionality.py @@ -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( diff --git a/authors/apps/authentication/tests/test_validations.py b/authors/apps/authentication/tests/test_validations.py index c158d43..21a483c 100644 --- a/authors/apps/authentication/tests/test_validations.py +++ b/authors/apps/authentication/tests/test_validations.py @@ -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): """ diff --git a/authors/apps/authentication/validations.py b/authors/apps/authentication/validations.py index 6ba3dbe..30ac9f2 100644 --- a/authors/apps/authentication/validations.py +++ b/authors/apps/authentication/validations.py @@ -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.' } ) diff --git a/authors/apps/profiles/models.py b/authors/apps/profiles/models.py index 4b57983..ce2aafe 100644 --- a/authors/apps/profiles/models.py +++ b/authors/apps/profiles/models.py @@ -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: diff --git a/authors/apps/profiles/serializers.py b/authors/apps/profiles/serializers.py index e2538cd..5115f26 100644 --- a/authors/apps/profiles/serializers.py +++ b/authors/apps/profiles/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import UserProfile +from .models import UserProfile, Follow class GetUserProfileSerializer(serializers.ModelSerializer): @@ -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') diff --git a/authors/apps/profiles/tests/__init__.py b/authors/apps/profiles/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authors/apps/profiles/tests/test_follow_users_feature.py b/authors/apps/profiles/tests/test_follow_users_feature.py new file mode 100644 index 0000000..a619c75 --- /dev/null +++ b/authors/apps/profiles/tests/test_follow_users_feature.py @@ -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) diff --git a/authors/apps/profiles/test_profile.py b/authors/apps/profiles/tests/test_profile.py similarity index 92% rename from authors/apps/profiles/test_profile.py rename to authors/apps/profiles/tests/test_profile.py index 3ad3f8a..1c8a942 100644 --- a/authors/apps/profiles/test_profile.py +++ b/authors/apps/profiles/tests/test_profile.py @@ -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): diff --git a/authors/apps/profiles/urls.py b/authors/apps/profiles/urls.py index 5da4515..4df8da7 100644 --- a/authors/apps/profiles/urls.py +++ b/authors/apps/profiles/urls.py @@ -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//follows/', FollowsView.as_view()), + path('profiles//followers/', FollowersView.as_view()), ] diff --git a/authors/apps/profiles/views.py b/authors/apps/profiles/views.py index 222ef58..78b58ff 100644 --- a/authors/apps/profiles/views.py +++ b/authors/apps/profiles/views.py @@ -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): @@ -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) \ No newline at end of file + 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)