Skip to content

Commit

Permalink
Feat(user following):Users should be able to follow each other
Browse files Browse the repository at this point in the history
Follow or unfollow other authors
Only authenticated users can follow/unfollow
Get list of followed authors
Get list of followers

[Delivers #162163272]
  • Loading branch information
bibangamba committed Dec 13, 2018
1 parent 22d0347 commit 1d8cb9c
Show file tree
Hide file tree
Showing 12 changed files with 471 additions and 24 deletions.
4 changes: 2 additions & 2 deletions authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ def create_user(self, username, email, password=None, social_id=None):

if email is None:
raise TypeError('Users must have an email address.')

user = self.model(username=username, email=self.normalize_email(email), social_id=social_id)
user = self.model(username=username, email=self.normalize_email(
email), social_id=social_id)
user.set_password(password)
user.save()

Expand Down
3 changes: 3 additions & 0 deletions authors/apps/authentication/test/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ def test_password_is_required(self):

class TestSocialAuthUsers(APITestCase):

def setUp(self):
self.client = APIClient()

def save_user_to_db(self, username='', email='', password=''):
user = {
'user': {
Expand Down
43 changes: 42 additions & 1 deletion authors/apps/profiles/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,45 @@

class ProfileDoesNotExist(APIException):
status_code = 400
default_detail = 'Profile does not exist.'
default_detail = 'Profile does not exist.'


class UserDoesNotExist(APIException):
status_code = 404
default_detail = 'User does not exist'


class FollowDoesNotExist(APIException):
status_code = 202
default_detail = 'You are not following this user.'

@staticmethod
def not_following_user(username):
status_code = 202
default_detail = 'You are not following {}.'.format(username)
return status_code, default_detail


class FollowingAlreadyException(APIException):
status_code = 409
default_detail = 'You are already following this user.'


class FollowSelfException(APIException):
status_code = 400
default_detail = 'You can not follow yourself.'


class NoFollowersException(APIException):
status_code = 200
default_detail = 'You do not have any followers.'


class NoFollowingException(APIException):
status_code = 200
default_detail = 'You are not following any users yet.'


class NotFollowingAnyUserException(APIException):
status_code = 200
default_detail = 'You are not following any user.'
23 changes: 23 additions & 0 deletions authors/apps/profiles/migrations/0002_follow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.1.3 on 2018-12-12 16:34

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('profiles', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='Follow',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('followed', models.IntegerField()),
('follower', models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
],
),
]
30 changes: 30 additions & 0 deletions authors/apps/profiles/migrations/0003_auto_20181212_1726.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 2.1.3 on 2018-12-12 17:26

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('profiles', '0002_follow'),
]

operations = [
migrations.AlterField(
model_name='follow',
name='followed',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='followed_user_id', to=settings.AUTH_USER_MODEL),
),
migrations.RemoveField(
model_name='follow',
name='follower',
),
migrations.AddField(
model_name='follow',
name='follower',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='follower_user_id', to=settings.AUTH_USER_MODEL),
),
]
21 changes: 18 additions & 3 deletions authors/apps/profiles/models.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
from django.db import models
from django.conf import settings
from ..authentication.models import User


class Profile(models.Model):
"""
This model defines a one to one relationship between a user and a profile
"""
user = models.OneToOneField(
'authentication.User', on_delete=models.CASCADE
)
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
image = models.URLField(blank=True)

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


class Follow(models.Model):
"""
This model defines a many to many relationship between a
user and a 'followed', another user they are following
It also defines a follower i.e. the user doing the following
"""
follower = models.ForeignKey(
User, on_delete=models.CASCADE, related_name='follower_user_id', null=True)
followed = models.ForeignKey(
User, on_delete=models.CASCADE, related_name='followed_user_id', null=True)

def __str__(self):
return "User with id: {} follows user with id: {}".format(self.follower.pk,
self.followed.pk)
30 changes: 29 additions & 1 deletion authors/apps/profiles/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.contrib.auth import authenticate
from rest_framework import serializers
from .models import Profile
from .models import Profile, Follow
from ..authentication.models import User
from .exceptions import *


class ProfileSerializer(serializers.ModelSerializer):
Expand All @@ -19,3 +21,29 @@ def get_image(self, obj):
return obj.image

return ''


class FollowSerializer(serializers.ModelSerializer):
profile = None

class Meta:
model = Follow
fields = ('follower', 'followed')


class FollowingSerializer(serializers.ModelSerializer):
""""
This class represents the 'get all the users I follow' serializer
"""
class Meta:
model = Follow
fields = ['follower']


class FollowerSerializer(serializers.ModelSerializer):
"""
This class represents the 'get all my followers' serializer
"""
class Meta:
model = Follow
fields = ['followed']
1 change: 1 addition & 0 deletions authors/apps/profiles/test/test_create_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def setUp(self):
'password': 'Sokosok1!'
}
}

def verify_account(self, token, uidb64):
request = APIRequestFactory().get(
reverse(
Expand Down
154 changes: 154 additions & 0 deletions authors/apps/profiles/test/test_follow_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""
This test suite covers all the tests for the 'follow a user' feature
"""

import json
from rest_framework.test import APITestCase, APIClient
from rest_framework.views import status
from ...authentication.models import User
from ..models import Follow


class TestFollowUser(APITestCase):

def setUp(self):
self.client = APIClient()
self.andrew = self.save_user(
'andrew', 'andrew@a.com', 'P@ssword23lslsn')
self.maria = self.save_user('maria', 'maria@a.com', 'P@ssword23lslsn')
self.juliet = self.save_user(
'julie', 'juliet@a.com', 'P@ssword23lslsn')
self.roni = self.save_user('roni', 'roni@a.com', 'P@ssword23lslsn')
self.sama = self.save_user('sama', 'samantha@a.com', 'P@ssword23lslsn')

def save_user(self, username, email, pwd):
validated_data = {'username': username,
'email': email, 'password': pwd}
return User.objects.create_user(**validated_data)

def return_verified_user_object(self, username='athena',
email='athena@a.com', password='P@ssword23lslsn'):
user = self.save_user(username, email, password)
return user

def test_user_trying_to_follow_is_not_authenticated(self):
res = self.client.post('/api/profiles/sama/follow')
self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(json.loads(res.content), {'profile': {
'detail': 'Authentication credentials were not provided.'}})

def test_user_followed_correctly(self):
verified_user = self.return_verified_user_object()
jwt = verified_user.token()
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + jwt)
res = self.client.post(
'/api/profiles/maria/follow')
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
self.assertEqual(json.loads(res.content), {'profile': {
'username': 'maria', 'bio': '', 'image': '', 'following': True}})

def test_user_cannot_follow_self(self):
verified_user = self.return_verified_user_object()
jwt = verified_user.token()
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + jwt)
res = self.client.post(
'/api/profiles/{}/follow'.format(verified_user.username))
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(json.loads(res.content), {'profile': {
'detail': 'You can not follow yourself.'}})

def test_user_follow_target_does_not_exist(self):
verified_user = self.return_verified_user_object()
jwt = verified_user.token()
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + jwt)
res = self.client.post(
'/api/profiles/josephine/follow')
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)

def test_user_already_followed(self):
verified_user = self.return_verified_user_object()
jwt = verified_user.token()
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + jwt)
self.client.post('/api/profiles/maria/follow')
res = self.client.post(
'/api/profiles/maria/follow')
self.assertEqual(res.status_code, status.HTTP_409_CONFLICT)
self.assertEqual(json.loads(res.content), {'profile': {
'detail': 'You are already following this user.'}})

def test_user_not_following_anyone(self):
verified_user = self.return_verified_user_object()
jwt = verified_user.token()
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + jwt)
res = self.client.get('/api/profiles/following')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(res.content), {'profile': {
'detail': 'You are not following any users yet.'}})

def test_get_all_following(self):
verified_user = self.return_verified_user_object()
jwt = verified_user.token()
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + jwt)
follow = Follow()
follow.follower_id = verified_user.pk
follow.followed_id = self.andrew.pk
follow.save()

res = self.client.get('/api/profiles/following')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(res.content), {'profile': {'following': [
{'username': 'andrew', 'bio': '', 'image': '', 'following': True}]}})

def test_get_all_followers(self):
verified_user = self.return_verified_user_object()
jwt = verified_user.token()
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + jwt)
follow = Follow()
follow.followed_id = verified_user.pk
follow.follower_id = self.roni.pk
follow.save()

res = self.client.get('/api/profiles/followers')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(res.content), {'profile': {'followers': [
{'username': 'roni', 'bio': '', 'image': '', 'following': True}]}})

def test_user_has_no_followers(self):
verified_user = self.return_verified_user_object()
jwt = verified_user.token()
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + jwt)
res = self.client.get('/api/profiles/followers')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(res.content), {'profile': {
'detail': 'You do not have any followers.'}})

def test_user_unfollows(self):
verified_user = self.return_verified_user_object()
jwt = verified_user.token()
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + jwt)
self.client.post('/api/profiles/maria/follow')
res = self.client.delete(
'/api/profiles/maria/follow')
self.assertEqual(res.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(json.loads(res.content), {'profile': {
'username': 'maria', 'bio': '', 'image': '', 'following': False}})

def test_user_unfollows_non_followed_user(self):
verified_user = self.return_verified_user_object()
jwt = verified_user.token()
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + jwt)
res = self.client.delete(
'/api/profiles/maria/follow')
self.assertEqual(res.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(json.loads(res.content), {'profile': {
'detail': 'You are not following this user.'}})
9 changes: 7 additions & 2 deletions authors/apps/profiles/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from django.urls import include, path
from django.conf.urls import url
from .views import ProfileRetrieveView
from .views import ProfileRetrieveView, FollowAPIView, FollowingAPIView, FollowersAPIView

urlpatterns = [
url('^profiles/(?P<slug>[-\w]+)$', ProfileRetrieveView.as_view(), name='profiles'),
path('profiles/following', FollowingAPIView.as_view()),
path('profiles/followers', FollowersAPIView.as_view()),
url('^profiles/(?P<slug>[-\w]+)$',
ProfileRetrieveView.as_view(), name='profiles'),
path('profiles/<username>/follow', FollowAPIView.as_view()),
]
Loading

0 comments on commit 1d8cb9c

Please sign in to comment.