Building a Social Media Site With Python and Django: Part 6 Followers

LegionScript
5 min readFeb 15, 2021

Video Tutorial

Code on Github

In this tutorial we are going to add the ability to follow and unfollow users. We will create two simple views that will handle the logic for this. We will need to update our models.py to have a field to store the followers for each user. Before we do this, we are going to update the post lists to have a link to go straight to the profile.

Adding a Profile Link on the Post Lists

First, we want to add a link to easily access the profile of each user. We will do this by changing the post lists in the social feed and in the profiles to have the user name go back to the profile, we will also add the @ symbol in front of it to make it clear that it is a username. Here is our updated listing of posts.

social/templates/social/post_list.html

{% for post in post_list %}
<div class="row justify-content-center mt-3">
<div class="col-md-5 col-sm-12 border-bottom position-relative">
<p><a style="text-decoration: none;" class="text-primary" href="{% url 'profile' post.author.profile.pk %}">@{{ post.author }}</a> {{ post.created_on }}</p>
<div class="position-relative">
<p>{{ post.body }}</p>
<a class="stretched-link" href="{% url 'post-detail' post.pk %}"></a>
</div>
</div>
</div>
{% endfor %}

social/templates/social/profile.html

{% for post in posts %}
<div class="row justify-content-center mt-5">
<div class="col-md-5 col-sm-12 border-bottom position-relative">
<p><a style="text-decoration: none;" class="text-primary" href="{% url 'profile' post.author.profile.pk %}">@{{ post.author }}</a> {{ post.created_on }}</p>
<div class="position-relative">
<p>{{ post.body }}</p>
<a class="stretched-link" href="{% url 'post-detail' post.pk %}"></a>
</div>
</div>
</div>
{% endfor %}

Updating Our Models.py

First we need to add a many to many relationship on our user profile to hold all of the followers for that user. Django has a ManyToMany field we can use for this. We need to make sure we use the User model as what will be stored in this field, we also want to make sure it can be left blank if the user has no followers. Make sure to migrate the changes with makemigrations and migrate after changing a models.py file.

social/models.py

class UserProfile(models.Model):
user = models.OneToOneField(User, primary_key=True, verbose_name='user', related_name='profile', on_delete=models.CASCADE)
name = models.CharField(max_length=30, blank=True, null=True)
bio = models.TextField(max_length=500, blank=True)
birth_date = models.DateField(null=True, blank=True)
location = models.CharField(max_length=100, blank=True, null=True)
picture = models.ImageField(upload_to='uploads/profile_pictures/', default='uploads/profile_pictures/default.png', blank=True)
followers = models.ManyToManyField(User, blank=True, related_name='followers')

Updating Profiles to Show Number of Followers

Now that we have everything set up we can display the number of followers for each user, it will be zero for now until we implement the rest of the logic. Let’s add the counting logic first so we can easily see if everything is working later. First we need to get the number of objects in the followers field and then we can display that in our template. We also want to add a boolean value to check if the user is following the other user, we will use this to decide whether to show a follow or an unfollow button on the profile.

social/views.py

class ProfileView(View):
def get(self, request, pk, *args, **kwargs):
profile = UserProfile.objects.get(pk=pk)
user = profile.user
posts = Post.objects.filter(author=user).order_by('-created_on')
followers = profile.followers.all() for follower in followers:
if follower == request.user:
is_following = True
break
else:
is_following = False
number_of_followers = len(followers) context = {
'user': user,
'profile': profile,
'posts': posts,
'is_following': is_following,
'number_of_followers': number_of_followers,
}
return render(request, 'social/profile.html', context)

social/templates/social/profile.html

<div class="row justify-content-center mt-5">
<div class="card col-md-8 col-sm-12 shadow-sm px-5 pt-3">
<img src="{{ profile.picture.url }}" class="rounded-circle" width="100" height="100" />
{% if profile.name %}
<h3 class="py-4">{{ profile.name }}
<span>
{% if request.user == user %}
<a href="{% url 'profile-edit' profile.pk %}" style="color: #333;"><i class="far fa-edit"></i></a>
{% endif %}
</span></h3>
{% endif %}
<div>
{% if profile.location %}
<p>{{ profile.location }}</p>
{% endif %}
{% if profile.birth_date %}
<p>{{ profile.birth_date }}</p>
{% endif %}
{% if profile.bio %}
<p>{{ profile.bio }}</p>
{% endif %}
</div>
<div class="mb-3">
<p>Followers: {{ number_of_followers }}</p>
</div>
</div>
</div>

Following View and URL Path

Now let’s add the view and url to follow a user, this view will take a post request and will add to the ManyToMany field we created earlier but instead of rendering a template, it will redirect back to the profile url that we were just on. Django has an add method for ManyToMany fields to add to it, so we can use that to easily add to it. We will use our generic View class and add a post method for this.

social/views.py

class AddFollower(LoginRequiredMixin, View):
def post(self, request, pk, *args, **kwargs):
profile = UserProfile.objects.get(pk=pk)
profile.followers.add(request.user) return redirect('profile', pk=profile.pk)

social/urls.py

from django.urls import path
from .views import PostListView, PostDetailView, PostEditView, PostDeleteView, CommentDeleteView, ProfileView, ProfileEditView, AddFollower, RemoveFollower
urlpatterns = [
path('', PostListView.as_view(), name='post-list'),
path('post/<int:pk>/', PostDetailView.as_view(), name='post-detail'),
path('post/edit/<int:pk>/', PostEditView.as_view(), name='post-edit'),
path('post/delete/<int:pk>/', PostDeleteView.as_view(), name='post-delete'),
path('post/<int:post_pk>/comment/delete/<int:pk>/', CommentDeleteView.as_view(), name='comment-delete'),
path('profile/<int:pk>/', ProfileView.as_view(), name='profile'),
path('profile/edit/<int:pk>/', ProfileEditView.as_view(), name='profile-edit'),
path('profile/<int:pk>/followers/add', AddFollower.as_view(), name='add-follower'),
]

Unfollowing View and URL Path

Now we will do the same thing for the unfollow view, this will be very similar, we can pretty much do the exact same thing except we can use the remove method on the ManyToMany field to remove an object from it. Then we can redirect back to the profile. We will use the generic view class with a post method again.

social/views.py

class RemoveFollower(LoginRequiredMixin, View):
def post(self, request, pk, *args, **kwargs):
profile = UserProfile.objects.get(pk=pk)
profile.followers.remove(request.user) return redirect('profile', pk=profile.pk)

social/urls.py

from django.urls import path
from .views import PostListView, PostDetailView, PostEditView, PostDeleteView, CommentDeleteView, ProfileView, ProfileEditView, AddFollower, RemoveFollower
urlpatterns = [
path('', PostListView.as_view(), name='post-list'),
path('post/<int:pk>/', PostDetailView.as_view(), name='post-detail'),
path('post/edit/<int:pk>/', PostEditView.as_view(), name='post-edit'),
path('post/delete/<int:pk>/', PostDeleteView.as_view(), name='post-delete'),
path('post/<int:post_pk>/comment/delete/<int:pk>/', CommentDeleteView.as_view(), name='comment-delete'),
path('profile/<int:pk>/', ProfileView.as_view(), name='profile'),
path('profile/edit/<int:pk>/', ProfileEditView.as_view(), name='profile-edit'),
path('profile/<int:pk>/followers/add', AddFollower.as_view(), name='add-follower'),
path('profile/<int:pk>/followers/remove', RemoveFollower.as_view(), name='remove-follower'),
]

Adding a Follow or Unfollow Button to the Profiles

Finally, lets add a button to send a post request to one of these two views, depending on if the user if following or not following the user. We checked for that in our view already so we should have access to an is_following boolean value. We can create two forms with a button that sends a post request, and show only one based on what that boolean value is. Here is the final version of that profile.html row that holds the follower information.

social/templates/social/profile.html

<div class="row justify-content-center mt-5">
<div class="card col-md-8 col-sm-12 shadow-sm px-5 pt-3">
<img src="{{ profile.picture.url }}" class="rounded-circle" width="100" height="100" />
{% if profile.name %}
<h3 class="py-4">{{ profile.name }}
<span>
{% if request.user == user %}
<a href="{% url 'profile-edit' profile.pk %}" style="color: #333;"><i class="far fa-edit"></i></a>
{% endif %}
</span></h3>
{% endif %}
<div>
{% if profile.location %}
<p>{{ profile.location }}</p>
{% endif %}
{% if profile.birth_date %}
<p>{{ profile.birth_date }}</p>
{% endif %}
{% if profile.bio %}
<p>{{ profile.bio }}</p>
{% endif %}
</div>
<div class="mb-3">
<p>Followers: {{ number_of_followers }}</p>
{% if user == request.user %}
{% else %}
{% if is_following %}

<form method="POST" action="{% url 'remove-follower' profile.pk %}">
{% csrf_token %}
<button class="btn btn-outline-danger" type=submit>UnFollow</button>
</form>
{% else %}
<form method="POST" action="{% url 'add-follower' profile.pk %}">
{% csrf_token %}
<button class="btn btn-outline-success" type=submit>Follow</button>
</form>
{% endif %}
{% endif %}
</div>
</div>
</div>

This concludes this tutorial, we still have more to add though so we will add more to this application again in the future.

--

--