Skip to content

Commit

Permalink
Merge 2a75242 into 4cc71b1
Browse files Browse the repository at this point in the history
  • Loading branch information
joelethan committed Mar 21, 2019
2 parents 4cc71b1 + 2a75242 commit f1ff2d0
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 14 deletions.
20 changes: 18 additions & 2 deletions authors/apps/profiles/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Generated by Django 2.1.5 on 2019-03-21 09:50

import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
Expand All @@ -14,18 +15,33 @@ class Migration(migrations.Migration):
]

operations = [
migrations.CreateModel(
name='Follow',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('followed_at', models.DateTimeField(blank=True, default=datetime.datetime.now)),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('display_name', models.CharField(blank=True, max_length=120, null=True)),
('bio', models.TextField(blank=True, null=True)),
('image', models.URLField(null=True)),
('following', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('followers', models.ManyToManyField(blank=True, related_name='is_following', to=settings.AUTH_USER_MODEL)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='follow',
name='followed',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.Profile'),
),
migrations.AddField(
model_name='follow',
name='follower',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]
12 changes: 8 additions & 4 deletions authors/apps/profiles/models.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
from django.db import models
from django.db.models.signals import post_save
from authors.apps.authentication.models import User
from datetime import datetime


class Profile(models.Model):
"""
Creates the profile model that will hold user profiles
Creates the profile model that will hold user profiles
"""

user = models.OneToOneField(User, on_delete=models.CASCADE)
display_name = models.CharField(max_length=120, blank=True, null=True)
followers = models.ManyToManyField(
User, related_name='is_following', blank=True)
bio = models.TextField(blank=True, null=True)
image = models.URLField(null=True)
following = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

Expand All @@ -38,3 +36,9 @@ def user_post_save_receiver(instance, created, *args, **kwargs):


post_save.connect(user_post_save_receiver, sender=User)


class Follow(models.Model):
follower = models.ForeignKey(User, on_delete=models.CASCADE)
followed = models.ForeignKey(Profile, on_delete=models.CASCADE)
followed_at = models.DateTimeField(default=datetime.now, blank=True)
8 changes: 7 additions & 1 deletion authors/apps/profiles/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from rest_framework import serializers

from .models import Profile
from .models import Profile, Follow


class ProfileSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -45,3 +45,9 @@ def to_representation(self, instance):
"image": instance.image or None, # put null if no image image url was set
}
}


class FollowSerializer(serializers.ModelSerializer):
class Meta:
model = Follow
fields = ('follower', 'followed', 'followed_at')
117 changes: 117 additions & 0 deletions authors/apps/profiles/tests/test_follow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from rest_framework import status
from django.urls import reverse
from .test_data import new_user, new_user_2
from authors.apps.profiles.tests.test_base import BaseTestCase


class FollowUsersTests(BaseTestCase):
def test_user_can_follow_another_user(self):
self.authorize_user_2()
self.authorize_user()
url = reverse('profiles:follow_user', kwargs={
'username': new_user_2['username']})
response = self.client.post(url, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertIn('Successfuly followed', response.data['message'])

def test_user_cannot_follow_a_user_that_doesnt_exist(self):
self.authorize_user()
url = reverse('profiles:follow_user', kwargs={'username': 'joel'})
response = self.client.post(url, format='json')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('Not found.', str(response.data))

def test_user_cannot_double_follow_user(self):
self.authorize_user_2()
self.authorize_user()
url = reverse('profiles:follow_user', kwargs={
'username': new_user_2['username']})
self.client.post(url, format='json')
response = self.client.post(url, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('You are already following ' +
str(new_user_2['username']), str(response.data))

def test_user_cannot_follow_self(self):
self.authorize_user()
url = reverse('profiles:follow_user', kwargs={
'username': new_user['username']})
response = self.client.post(url, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('You cannot follow your own profile.', str(response.data))

def test_user_can_unfollow_another_user(self):
self.authorize_user_2()
self.authorize_user()
url = reverse('profiles:follow_user', kwargs={
'username': new_user_2['username']})
self.client.post(url, format='json')
response = self.client.delete(url, format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertIn('You have successfully unfollowed ' +
str(new_user_2['username']), str(response.data))

def test_user_cannot_unfollow_a_user_they_are_not_following(self):
self.authorize_user_2()
self.authorize_user()
url = reverse('profiles:follow_user', kwargs={
'username': new_user_2['username']})
response = self.client.delete(url, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('You cannot unfollow a user you are not following.', str(response.data))

def test_user_cannot_unfollow_a_user_that_doesnt_exist(self):
self.authorize_user()
url = reverse('profiles:follow_user', kwargs={'username': 'joel'})
response = self.client.delete(url, format='json')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('Not found.', str(response.data))

def test_return_list_of_followings_for_a_user(self):
self.authorize_user_2()
self.authorize_user()
url = reverse(
'profiles:follow_user', kwargs={'username': new_user_2['username']})
get_url = reverse(
'profiles:follow_user', kwargs={'username': new_user['username']})
self.client.post(url, format='json')
response = self.client.get(get_url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('following', str(response.data))
self.assertEqual(response.data['following_count'], 1)
self.assertIn('followed_at', str(response.data))

def test_return_followings_of_a_user_that_doesnt_exist(self):
self.authorize_user()
url = reverse('profiles:follow_user', kwargs={'username': 'joel'})
response = self.client.get(url, format='json')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('Not found.', str(response.data))

def test_return_followers_for_a_user_that_doesnt_exist(self):
self.authorize_user()
url = reverse('profiles:get_followers', kwargs={'username': 'joel'})
response = self.client.get(url, format='json')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('Not found.', str(response.data))

def test_user_with_no_followers(self):
self.authorize_user_2()
self.authorize_user()
url = reverse(
'profiles:get_followers', kwargs={'username': new_user_2['username']})
response = self.client.get(url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('has no followers', str(response.data))

def test_return_followers_of_a_user(self):
self.authorize_user_2()
self.authorize_user()
url = reverse('profiles:get_followers', kwargs={
'username': new_user_2['username']})
get_url = reverse('profiles:get_followers', kwargs={
'username': new_user_2['username']})
self.client.post(url, format='json')
response = self.client.get(get_url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('has no followers', response.data['message'])
10 changes: 9 additions & 1 deletion authors/apps/profiles/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.urls import path
from .views import (ProfileRetrieveAPIView,
from .views import (ProfileRetrieveAPIView, FollowsView, FollowersView,
ListAuthorsAPIView, ProfileUpdateAPIView, AuthorsAPIView)

urlpatterns = [
Expand All @@ -23,4 +23,12 @@
AuthorsAPIView.as_view(),
name="authors_list"
),
path(
'profiles/<username>/follows/',
FollowsView.as_view(),
name="follow_user"),
path(
'profiles/<username>/followers/',
FollowersView.as_view(),
name="get_followers")
]
120 changes: 114 additions & 6 deletions authors/apps/profiles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
from authors.apps.authentication.models import User
from authors.apps.authentication.renderers import UserJSONRenderer
from authors.apps.authentication.serializers import UserSerializer

from .models import Profile
from .models import Profile, Follow
from .permissions import IsOwnerOrReadOnly

from .renderers import ProfileJSONRenderer
from .serializers import ProfileSerializer, ProfileUpdateSerializer
from .serializers import \
ProfileSerializer, ProfileUpdateSerializer, FollowSerializer
import datetime


class ProfileRetrieveAPIView(generics.RetrieveAPIView):
Expand All @@ -35,7 +35,8 @@ def retrieve(self, request, username, *args, **kwargs):


class ProfileUpdateAPIView(generics.UpdateAPIView):
""" Allows the currently logged in user
"""
Allows the currently logged in user
to edit their user profile
"""
serializer_class = ProfileUpdateSerializer
Expand All @@ -47,7 +48,6 @@ def get_object(self):
return obj



class ListView(generics.ListAPIView):
permission_classes = (permissions.IsAuthenticated, )

Expand All @@ -68,3 +68,111 @@ class AuthorsAPIView(ListView):
queryset = User.objects.all()
serializer_class = UserSerializer
renderer_classes = (UserJSONRenderer,)


class FollowsView(generics.CreateAPIView, generics.DestroyAPIView):
permission_classes = (permissions.IsAuthenticated,)
renderer_classes = (ProfileJSONRenderer,)
serializer_class = FollowSerializer

def post(self, request, username):
"""
User follows an Author
"""
follower = User.objects.get(username=request.user.username)
followed = get_object_or_404(User, username=username)
check = self.check_follow_status(follower, followed)
if isinstance(check, Response):
return check
follow_data = {
'follower': follower.id,
'followed': followed.profile.id}
serializer = self.serializer_class(data=follow_data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response({
"message": "Successfuly followed {}".format(followed.username)
}, status=status.HTTP_201_CREATED)

def check_follow_status(self, follower, followed):
if follower.id == followed.id:
return Response({
'error': 'You cannot follow your own profile.'
}, status=status.HTTP_400_BAD_REQUEST)
query_result = Follow.objects.filter(
follower_id=follower.id, followed_id=followed.profile.id).first()
if query_result:
return Response({
"error": "You are already following {}.".format(followed.username)
}, status=status.HTTP_400_BAD_REQUEST)

def delete(self, request, username):
"""
User unfollows an Author
"""
follower = User.objects.get(username=request.user.username)
followed = get_object_or_404(User, username=username)
follow = Follow.objects.filter(
follower_id=follower.id, followed_id=followed.profile.id).first()
if not follow:
return Response({
'error': 'You cannot unfollow a user you are not following.'
}, status=status.HTTP_400_BAD_REQUEST)
follow.delete()
return Response(
{'message': 'You have successfully unfollowed {}.'.format(
username)},
status=status.HTTP_204_NO_CONTENT)

def get(self, request, username):
"""
Get all Authors a user is following
"""
user = get_object_or_404(User, username=username)
follows = Follow.objects.filter(follower_id=user.id)
serializer = self.serializer_class(follows, many=True)
following_list = list()
for follow in serializer.data:
profile = Profile.objects.get(id=follow['followed'])
following_list.append({
"username": profile.user.username,
"bio": profile.bio,
"image": profile.image,
"followed_at": follow['followed_at']
})
if len(following_list) == 0:
response = {'message': '{} has no following'.format(username)}
response = {'following_count': len(
following_list), 'following': following_list}
return Response(response, status=status.HTTP_200_OK)


class FollowersView(generics.ListAPIView):
permission_classes = (permissions.IsAuthenticated,)
renderer_classes = (ProfileJSONRenderer,)
serializer_class = FollowSerializer

def get(self, request, username):
"""
Get all Users following an Author
"""
user = get_object_or_404(User, username=username)
followers = Follow.objects.filter(followed_id=user.id)
serializer = self.serializer_class(followers, many=True)
followers_list = list()
for follow in serializer.data:
profile = Profile.objects.get(id=follow['followed'])
user = User.objects.get(id=follow['follower'])
followers_list.append({
"username": user.username,
"bio": profile.bio,
"image": profile.image,
"followed_at": follow['followed_at']
})
if not followers_list:
response = {
'message': '{} has no followers'.format(username)}
else:
response = {
'follower_count': len(followers_list), 'followers': followers_list}
return Response(response, status=status.HTTP_200_OK)

0 comments on commit f1ff2d0

Please sign in to comment.