-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
471 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
30
authors/apps/profiles/migrations/0003_auto_20181212_1726.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.'}}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()), | ||
] |
Oops, something went wrong.