Follow Button Logic and Endpoint:

Now we need to prepare to import a button on HTML page to let users can manually organize their following or followed.

profiles/api:

  • create directory /profiles/api
  • and create 3 files
    • /profiles/api/init.py
      • (empty)
    • /profiles/api/urls.py ```python from django.urls import path

from .views import ( user_follow_view, )

urlpatterns = [ path(‘<str:username/follow’, user_follow_view), ]

  1. - /profiles/api/views.py
  2. ```python
  3. import random
  4. from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
  5. from django.shortcuts import render, redirect
  6. from django.utils.http import is_safe_url
  7. from django.conf import settings
  8. from django.contrib.auth import get_user_model
  9. from rest_framework.authentication import SessionAuthentication
  10. from rest_framework.decorators import api_view, permission_classes, authentication_classes
  11. from rest_framework.permissions import IsAuthenticated
  12. from rest_framework.response import Response
  13. from ..models import Profile
  14. User = get_user_model()
  15. ALLOWED_HOSTS = settings.ALLOWED_HOSTS
  16. @api_view(['GET', 'POST'])
  17. @permission_classes([IsAuthenticated])
  18. def user_follow_view(request, username, *args, **kwargs):
  19. me = request.user
  20. other_user_qs = User.objects.filter(username=username)
  21. if not other_user_qs.exists():
  22. return Response({}, status=404)
  23. other = other_user_qs.first()
  24. profile = other.profile
  25. data = request.data or {}
  26. action = data.get("action")
  27. print(data)
  28. if action == "follow": # if I am not following this profile
  29. profile.followers.add(me) # start following
  30. elif action == "unfollow": # else
  31. profile.followers.remove(me) # stop following
  32. else:
  33. pass
  34. current_followers_qs = profile.followers.all()
  35. return Response({"count": current_followers_qs.count()}, status=200)
  36. # we don't just use profile.followers.all() here because 'User' is not JSON serializable
  • add a path for api url in /Twittme/urls.py

    1. # ...
    2. urlpatterns = [
    3. path('admin/', admin.site.urls),
    4. path('api/tweets/', include('tweets.api.urls')),
    5. path('', tweets_list_view),
    6. path('login/', login_view),
    7. path('logout/', logout_view),
    8. path('register/', register_view),
    9. path('<int:tweet_id>', tweets_detail_view),
    10. re_path(r'profiles?/', include('profiles.urls')),
    11. re_path(r'api/profiles?/', include('profiles.api.urls')), # new
    12. ]
    13. # ...

Test case:

to test, runserver
access http://localhost/api/profiles/[username]/follow

we apply username as “root” here,
count is 0 right now
image.png
we POST content {“action”: “follow”}
image.png
and count becomes 1
image.png

This time we POST content {“action”: “unfollow”}
image.png
count is reduced to 0
image.png

But if we POST content {“action”: “unfollow”} again
image.png
count won’t be -1
image.png

Same as {“action”: “follow”} when count is 1
image.png
count won’t be 2
image.png

That’s because it’s actually keep adding and removing the same user who follows a profile, and 1 user is either following or not following a profile.

“data” in /profiles/api/views.py will always be {“action”: “follow”} or {“action”: “unfollow”}
**

Profile Following Unit Test Cases:

In this step, we can try to setup a unit test cases for profiles/api

Test case:

  • add new contents in /profiles/tests.py ```python from django.contrib.auth import get_user_model from django.test import TestCase

from rest_framework.test import APIClient # new

from .models import Profile

User = get_user_model()

class ProfileTestCase(TestCase): def setUp(self): self.user = User.objects.create_user( username=’cfe’, password=’somepassword’) self.userb = User.objects.create_user( username=’cfe-2’, password=’somepassword2’)

  1. def get_client(self): # new
  2. client = APIClient()
  3. client.login(username=self.user.username, password='somepassword')
  4. return client
  5. def test_profile_created_via_signal(self):
  6. qs = Profile.objects.all()
  7. self.assertEqual(qs.count(), 2)
  8. def test_following(self): # new, to test following step
  9. first = self.user
  10. second = self.userb
  11. first.profile.followers.add(second) # added a follower
  12. second_user_following_whom = second.following.all()
  13. # from a user, check other user is being followed.
  14. qs = second_user_following_whom.filter(user=first)
  15. # check new user has is not following anyone
  16. first_user_following_no_one = first.following.all()
  17. self.assertTrue(qs.exists())
  18. self.assertFalse(first_user_following_no_one.exists())
  19. def test_follow_api_endpoint(self): # new, to test unfollowing step
  20. client = self.get_client()
  21. response = client.post(f"/api/profiles/{self.userb.username}/follow", {"action": "follow"})
  22. r_data = response.json()
  23. count = r_data.get("count")
  24. self.assertEqual(count, 1)
  25. def test_unfollow_api_endpoint(self):
  26. first = self.user
  27. second = self.userb
  28. first.profile.followers.add(second) # add a follower
  29. client = self.get_client()
  30. response = client.post(
  31. f"/api/profiles/{self.userb.username}/follow",
  32. {"action": "unfollow"}
  33. )
  34. r_data = response.json()
  35. count = r_data.get("count")
  36. self.assertEqual(count, 0)
  1. to test, run test case in the terminal
  2. ```bash
  3. [root@localhost Twittme]# python3 ./manage.py test profiles
  4. Creating test database for alias 'default'...
  5. System check identified no issues (0 silenced).
  6. <QueryDict: {'action': ['follow']}>
  7. ...<QueryDict: {'action': ['unfollow']}>
  8. .
  9. ----------------------------------------------------------------------
  10. Ran 4 tests in 3.028s
  11. OK
  12. Destroying test database for alias 'default'...

all cases are passed, so that the following and unfollowing step is successful.

“Not the same user”:

There is a problem that we cannot let a user follow itself.

  • add this checking in /profiles/api/views.py

    1. @api_view(['GET', 'POST'])
    2. @permission_classes([IsAuthenticated])
    3. def user_follow_view(request, username, *args, **kwargs):
    4. me = request.user
    5. other_user_qs = User.objects.filter(username=username)
    6. if me.username == username: # to check is follower and followed use the same one
    7. my_followers = me.profile.followers.all()
    8. return Response({"count": my_followers.count()}, status=200)
    9. if not other_user_qs.exists():
    10. return Response({}, status=404)
    11. other = other_user_qs.first()
    12. profile = other.profile
    13. data = request.data or {}
    14. action = data.get("action")
    15. print(data)
    16. if action == "follow": # if I am not following this profile
    17. profile.followers.add(me) # start following
    18. elif action == "unfollow": # else
    19. profile.followers.remove(me) # stop following
    20. else:
    21. pass
    22. current_followers_qs = profile.followers.all()
    23. return Response({"count": current_followers_qs.count()}, status=200)
  • add the test case about “a use cannot follow oneself” in /profiles/tests.py

    1. # ...
    2. def test_cannot_follow_api_endpoint(self): # test the setting that a user cannot follow itself
    3. client = self.get_client()
    4. response = client.post(
    5. f"/api/profiles/{self.user.username}/follow",
    6. {"action": "follow"}
    7. )
    8. r_data = response.json()
    9. count = r_data.get("count")
    10. self.assertEqual(count, 0)

    to test, run test case in the terminal ```bash [root@localhost Twittme]# python3 ./manage.py test profiles Creating test database for alias ‘default’… System check identified no issues (0 silenced). . .


Ran 5 tests in 4.101s

OK Destroying test database for alias ‘default’…

``` all cases are passed, so that a user cannot follow itself.