Skip to content

Commit

Permalink
Merge pull request #382 from bounswe/backend-follow-endpoint
Browse files Browse the repository at this point in the history
Add follow/unfollow functionality with tests and Swagger docs
  • Loading branch information
sonerkuyar authored Dec 14, 2024
2 parents 37fbe60 + 8f3499a commit 6c5937c
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 3 deletions.
27 changes: 27 additions & 0 deletions app/backend/v1/apps/accounts/migrations/0002_follow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.1.4 on 2024-12-14 11:16

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


class Migration(migrations.Migration):

dependencies = [
('accounts', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='Follow',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('follower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following', to=settings.AUTH_USER_MODEL)),
('following', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='followers', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('follower', 'following')},
},
),
]
12 changes: 12 additions & 0 deletions app/backend/v1/apps/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@

class CustomUser(AbstractUser):
pass

class Follow(models.Model):
follower = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name="following")
following = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name="followers")
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
unique_together = ('follower', 'following') # Each follower-following pair is unique

def __str__(self):
return f"{self.follower.username} follows {self.following.username}"

33 changes: 31 additions & 2 deletions app/backend/v1/apps/accounts/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from v1.apps.accounts.models import CustomUser
from v1.apps.accounts.models import CustomUser, Follow
from django.test import TestCase


class AccountsTests(APITestCase):
def setUp(self):
Expand Down Expand Up @@ -79,3 +81,30 @@ def test_login_nonexistent_user(self):
response = self.client.post(self.login_url, login_data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("Invalid credentials", response.data['error'])

class FollowUserTest(TestCase):
def setUp(self):
self.client = APIClient()
self.follower = CustomUser.objects.create_user(username="follower", password="password")
self.following = CustomUser.objects.create_user(username="following", password="password")
self.url = reverse('toggle-follow', args=[self.following.id])

def test_follow_user_authenticated(self):
self.client.force_authenticate(user=self.follower)
response = self.client.post(self.url)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
follow_exists = Follow.objects.filter(follower=self.follower, following=self.following).exists()
self.assertTrue(follow_exists)

def test_unfollow_user_authenticated(self):
self.client.force_authenticate(user=self.follower)
self.client.post(self.url) # Follow first
response = self.client.post(self.url) # Then unfollow
self.assertEqual(response.status_code, status.HTTP_200_OK)
follow_exists = Follow.objects.filter(follower=self.follower, following=self.following).exists()
self.assertFalse(follow_exists)

def test_follow_user_unauthenticated(self):
response = self.client.post(self.url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

1 change: 1 addition & 0 deletions app/backend/v1/apps/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
urlpatterns = [
path('signup/', views.sign_up, name='sign-up'),
path('login/', views.login, name='login'),
path('<int:user_id>/follow/', views.toggle_follow, name='toggle-follow'),
]
57 changes: 56 additions & 1 deletion app/backend/v1/apps/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from drf_yasg import openapi
from rest_framework.permissions import AllowAny
from rest_framework.decorators import permission_classes
from v1.apps.accounts.models import CustomUser, Follow
from rest_framework.permissions import IsAuthenticated

User = get_user_model()

# Sign-up view
Expand Down Expand Up @@ -90,4 +93,56 @@ def login(request):
'refresh_token': str(refresh)
}, status=status.HTTP_200_OK)
else:
return Response({"error": "Invalid credentials"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": "Invalid credentials"}, status=status.HTTP_400_BAD_REQUEST)


@swagger_auto_schema(
method='post',
operation_description="Toggle follow/unfollow for a specific user. If the user is not followed, the current user will follow them. If the user is already followed, the follow will be removed.",
operation_summary="Toggle Follow on a User",
responses={
201: openapi.Response(
description="User followed successfully",
examples={
'application/json': {
'message': 'User followed successfully'
}
}
),
200: openapi.Response(
description="User unfollowed successfully",
examples={
'application/json': {
'message': 'User unfollowed successfully'
}
}
),
404: openapi.Response(
description="User not found",
examples={
'application/json': {
'error': 'User not found'
}
}
)
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def toggle_follow(request, user_id):
try:
following_user = CustomUser.objects.get(id=user_id)
except CustomUser.DoesNotExist:
return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)

follower = request.user
if follower == following_user:
return Response({"error": "You cannot follow yourself"}, status=status.HTTP_400_BAD_REQUEST)

follow_instance, created = Follow.objects.get_or_create(follower=follower, following=following_user)

if not created:
follow_instance.delete()
return Response({"message": "User unfollowed successfully"}, status=status.HTTP_200_OK)

return Response({"message": "User followed successfully"}, status=status.HTTP_201_CREATED)

0 comments on commit 6c5937c

Please sign in to comment.