Follow and unfollow button

I’m trying to create a follow and unfollow button that checks if a user has followed another profile, and if the user hasn’t, an option to follow the profile would show.

I’ve tried two different options but still struggling with getting it right.

MODELS.PY

class Followers(models.Model):
    follower = models.ForeignKey('User', on_delete=models.CASCADE, null=True, related_name="follows_user")
    follows = models.ManyToManyField(User, related_name="followed_by", symmetrical=False, blank=True)

    def __str__(self):
        return str(self.follower)

PROFILE.HTML

        <form method="POST">
            {% csrf_token %}
            <h2>{{ profile }}</h2>
            <p>@{{ profile }}</p>
            <input type="hidden" value="{{user.username}}" name="follower">
            <input type="hidden" value="{{user_object.username}}" name="user">

            <!-- new -->
            {% if profile != request.user %}
                {% if profile in profile.follows.all %}
                    <button type="submit" class="btn btn-primary rounded-pill my-1 mx-1 py-1 py-1" name="follow" value="unfollow">Unfollow</button>
                {% else %}
                    <button type="submit" class="btn btn-primary rounded-pill my-1 mx-1 py-1 py-1" name="follow" value="follow">Follow</button>
                {% endif %}
            {% else %}
                <a href="{% url 'logout' %}">Log Out</a>
            {% endif %}
            
                <strong>Followed by</strong> <br/>
                {% for following in profile.followed_by.all %}
                    {{ following }}
                {% endfor %}


                <strong>Follows</strong> <br/>
                {% for i in profile.follows_user.all %}
                    @ {{ i }} <br/>
                {% endfor %}


        </form>

VIEWS.PY

def profile(request, user_id):
    profile = User.objects.get(pk = user_id)
    # following = Followers.objects.get(pk = user_id)
    if request.user.is_authenticated:
        if request.method == "POST":
            # get current user id
            current_user_profile = request.user
            # get form data
            action = request.POST['follow']
            # confirming follow status via action
            if action == "unfollow":
                current_user_profile.followed_by.remove(profile)
            else:
                  current_user_profile.follows.add(profile)  
            current_user_profile.save()  
        
        context = {
            "profile": profile,
        }
        return render(request, "network/profile.html", context)
    else:
        messages.INFO(request, ("You must be logged in"))
    return HttpResponseRedirect(reverse("index"))

The struggle I’m having with the above option is that in the below code rather than the followed user (“followee”) showing, the currently logged in user or the follower would show e.g Test1 follows Test2, but instead of Test2 showing, it shows Test1.

When i click on the follow button, it shows

 "AttributeError at /profile/2 -'User' object has no attribute 'follows' - C:\Users\hp\Desktop\CS50\network\network\views.py, line 144, in profile
                  current_user_profile.follows.add(profile)

Then I tried to edit my views

VIEWS2.PY

def profile(request, user_id):
    profile = User.objects.get(pk = user_id)
    # following = Followers.objects.get(pk = user_id)
    if request.user.is_authenticated:
        if request.method == "POST":
            follower = request.POST['follower']
            follows = request.user

            if Followers.objects.filter(follower=follower, follows=follows).first():
                delete_follower = Followers.objects.get(follower=follower, follows=follows)
                delete_follower.delete()
                following = False
                context = {
                    "profile": profile,
                    "following": following,
                    "follows": follows,
                }

                return render(request, "network/profile.html", context)
            else:
                new_follower = Followers.objects.create(follower=follower, follows=follows)
                new_follower.save()
                following = True
                context = {
                    "profile": profile,
                    "following": following,
                    "follows": follows,
                }

                return render(request, "network/profile.html", context)
    else:
        messages.INFO(request, ("You must be logged in"))
    return HttpResponseRedirect(reverse("index"))

PROFILE2.HTML

        <form method="POST">
            {% csrf_token %}
            <h2>{{ profile }}</h2>
            <p>@{{ profile }}</p>
            <input type="hidden" value="{{user.username}}" name="follower">
            <input type="hidden" value="{{user_object.username}}" name="user">

            <!-- new -->
            {% if profile != request.user %}
                {% if profile in profile.follows.all %}
                    <button type="submit" class="btn btn-primary rounded-pill my-1 mx-1 py-1 py-1" name="follow" value="unfollow">Unfollow</button>
                {% else %}
                    <button type="submit" class="btn btn-primary rounded-pill my-1 mx-1 py-1 py-1" name="follow" value="follow">Follow</button>
                {% endif %}
            {% else %}
                <a href="{% url 'logout' %}">Log Out</a>
            {% endif %}


        </form>

This second option immediately redirects me to the index page.

I’m having a hard time understanding your modelling here.

What is the purpose of having both follower and follows as fields in this model? I’m not sure I’m understanding why you’re creating a structure like this. (It seems to me that you’d be better off with two ForeignKey fields rather than trying to combine an FK with an MTM, effectively making the Followers model the through table of a many-to-many relationship of User with itself.)

1 Like

Follower should indicate who is currently following the currently-logged-in-user while follow is the currently-logged-in-user.

At first, i made follower a OneToOneField but realized that that would mean the follower can only follower one person, that’s why I changed it to a ForeignKey.

I am working on the edit to try to achieve the follow/unfollow button.

Yes, but with the ForeignKey, then the other link in that model should also be a ForeignKey, not a many-to-many. The issue here is that you’re creating a situation where it’s possible to create multiple distinct and redundant “follow” connections.

The rest of your code will be easier if you clean the model design first.

1 Like

Yes, I kind of did a rearrangement (a lot) to try to achieve this follow/unfollow button because I’ve been having a hard time making it work. Here’s my latest go at it

I amgetting ‘TypeError at /follow/4 follow() got an unexpected keyword argument ‘user_id’’ with the below code;

MODESL.PY

class User(AbstractUser):
    following = models.ManyToManyField(
        "self", blank=True, related_name="followers", symmetrical=False
    )

    def __str__(self):
        return str(self.username)

URL.PY

path("profile/<int:user_id>", views.profile, name="profile"),
path("follow/<int:user_id>", views.follow, name="follow"),

PROFILE.HTML

<div class="row">
    <b> following : </b>
    <p class="text-muted"> {{ profile.following.count }} </p>
    &nbsp;&nbsp;&nbsp;&nbsp;
    <b> followers : </b>
    <p class="text-muted"> {{ profile.followers.count }} </p>
</div>

{% if user.is_authenticated %}
    {% if user in profile.following.all %}
        <a href="{% url 'follow' user.id  %}" class="btn btn-primary">Unfollow</a>
    {% else %}
        <a  href="{% url 'follow' user.id  %}" class="btn btn-primary"> Follow </a>
    {% endif %}
{% else %}
    <button class="btn btn-outline-primary">Message</button>    
    <p class="text-muted"> please, login to follow </p>
{% endif %}

VIEWS.PY

def profile(request, user_id):
    profile = User.objects.get(pk = user_id)  
    context = {
        "profile": profile,
    }
    return render(request, "network/profile.html", context)

def follow(request, user_id):
    authorObj = User.objects.get(pk=user_id)
    currentUserObj = User.objects.get(pk=request.user.pk)
    following = authorObj.following.all()

    if user_id != currentUserObj.pk:
        if currentUserObj in following:
            authorObj.following.remove(currentUserObj)
        else:
            authorObj.following.add(currentUserObj)

    return HttpResponseRedirect(reverse("index"))

I suspect the error is coming from the views

def follow(request, user_id):

searching on the template for the argument 'user_id which I’ve checked and the following user.id should resolve the issue but still I’m getting the same error

``{% if user in profile.following.all %}
        <a href="{% url 'follow' user.id  %}" class="btn btn-primary">Unfollow</a>
    {% else %}
        <a  href="{% url 'follow' user.id  %}" class="btn btn-primary"> Follow </a>
    {% endif %}
{% else %}``

It would be helpful to see the complete traceback. That should more closely identify the source of the error.

You’ll also want to confirm that you don’t have another view named follow in your code.

1 Like

I checked and made the mistake of having another view that I thought I had commented out. Thank you.

When I click on follow or unfollow, the button is not adding or removing the user. Could you point me to where I am not getting it?

Something that I think leads toward possible confusion is the use of the followers / follows terminology. We tend to prefer terms that, while maybe not as precisely correct, tend to aid clarity and reduce that chance for confusion - in this case, we might use something like authors and fans.

In this case, your model might look like this:

class User(AbstractUser):
    authors = models.ManyToManyField(
        "self", blank=True, related_name="fans", symmetrical=False
    )

So that for some User named user, user.fans.all() are the people following user, while user.authors.all() are the set of people that user is following.

Now, for the view in question:

def follow(request, user_id):
    authorObj = User.objects.get(pk=user_id)
    currentUserObj = User.objects.get(pk=request.user.pk)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    following = authorObj.following.all()

    if user_id != currentUserObj.pk:
        if currentUserObj in following:
            authorObj.following.remove(currentUserObj)
        else:
            authorObj.following.add(currentUserObj)

    return HttpResponseRedirect(reverse("index"))

The undermarked query is redundant. The request.user object is currentUserObj.

Also, there’s no need to retrieve the current set of all of a relationship (in either direction). This means you don’t need either the authorObj = User.objects.get(pk=user_id) or the following = authorObj.following.all() query.

What you are looking to determine at this point is whether the current user is following (“is a fan of”) the author identified by the URL parameter (authorObj).

This conditional:
if request.user.authors.filter(pk=user_id).exists():
will check to see if the person currently logged on (request.user) currently has the author (identified by the pk in the url) in their M2M relationship.

If this condition is true, then you want to remove that entry:
request.user.authors.remove(user_id)

If the condition is false, you want to add a new entry:
request.user.authors.add(user_id)

(Yes, the add and remove functions work with PK values as well as with the object references.)

1 Like

Hi Ken, I ran into an issue and I’ve been trying to figure out where the error is coming from and ran some tweaks but still stuck at it.

def follow(request, user_id):
    authorObj = User.objects.get(pk=user_id)
    if user_id == request.user.id:
        if request.user.authors.filter(pk=user_id).exists():
            request.user.authors.remove(user_id)
        else:
            request.user.authors.add(user_id)

    return HttpResponseRedirect(reverse('profile', args=[authorObj.id]))

When I click on the Follow button for Profile 2 for example, instead of Unfollow button to show on the Profile 2 to indicate that I am currently following Profile 2 and can therefore unfollow it, it shows on Profile 1 (which is the currently logged in profile). Then the followers count for Profile 1 would increase which should show in the Profile 2 as seen below;

Then, the followers (authors) count for Profile 1 and the following (fans) profile for Profile 1 count increases instead of just the following count for Profile 1 to increase. Also, the Unfollow button shows in Profile 1 instead of when i switch to Profile 2.

I also ran into the same issue when using the previous code, so my suspicion is that adding the followers/author M2Mfield to User has tied the following/unfollowing relationship to the currently logged in user.

def follow(request, user_id):
    authorObj = User.objects.get(pk=user_id)
    # currentUserObj = request.user
    # following = currentUserObj.authors.all()

    # if user_id != currentUserObj:
    #     if authorObj in following:
    #         currentUserObj.authors.remove(authorObj)
    #     else:
    #         currentUserObj.authors.add(authorObj)

I wanted to get your opinion on this or to find out where I am making the mistake.

Thanks

Under what condition is that if going to be True?

That if is only going to be True if the person making the request (request.user) is trying to follow themself (user_id from the url parameter)

1 Like

Yes, this was a trial, the actual code I have is

if user_id != request.user:

Is there still an issue then?

1 Like

Yes, let’s say I’m currently logged in as Profile 1, then I go to Profile 2’s url “(http://127.0.0.1:8000/profile/2)” and click on the Follow button on Profile 2, which should mean that Profile 1 is currently following Profile 2 and Profile 2 button should update to Unfollow.

I’d instead get Profile 1 both as the Followed user (author) and the Following user (fan) and the Unfollow button would show on (http://127.0.0.1:8000/profile/1) instead of profile 2

I need to see the code that you’re having problems with. What is the view that is giving you these results?

1 Like

This is my views:

def follow(request, user_id):
    authorObj = User.objects.get(pk=user_id)
    if user_id != request.user:
        if request.user.authors.filter(pk=request.user.id).exists():
            request.user.authors.remove(request.user)
        else:
            request.user.authors.add(request.user)

    return HttpResponseRedirect(reverse('profile', args=[authorObj.id]))

My profile.html

        {% for following in profile.authors.all %}
            {{ following }}
        {% endfor %}

        {% for following in profile.fans.all %}
            {{ following }}
        {% endfor %}

        {% if user.is_authenticated %}
            {% if user in profile.authors.all %}
                <a href="{% url 'follow' user.id  %}" class="btn btn-primary">Unfollow</a>
            {% else %}
                <a  href="{% url 'follow' user.id  %}" class="btn btn-primary"> Follow </a>
            {% endif %}
        {% else %}
            <button class="btn btn-outline-primary">Message</button>    
            <p class="text-muted"> please, login to follow </p>
        {% endif %}

Notice what you’re doing here: You’re adding request.user to request.user

1 Like

This works the same as changing the code to

        if request.user.authors.filter(pk=request.user.id).exists():
            request.user.authors.remove(user_id)
        else:
            request.user.authors.add(user_id)

I have the same author and fans showing as Test1

Please show the view that is rendering that page.

This is my Profile and Follow view:

def profile(request, user_id):
    profile = User.objects.get(pk = user_id)  
    post_count = NewTweet.objects.filter(user = user_id).count()
    context = {
        "profile": profile,
        "post_count": post_count,
    }
    return render(request, "network/profile.html", context)

def follow(request, user_id):
    authorObj = User.objects.get(pk=user_id)
    if user_id != request.user:
        if request.user.authors.filter(pk=user_id).exists():
            request.user.authors.remove(user_id)
        else:
            request.user.authors.add(user_id)

    return HttpResponseRedirect(reverse('profile', args=[authorObj.id]))

And confirming that this is still your User model?

1 Like