Followers / following with AbstractUser model

Hello, new to django and here…

I’m finding it difficult to implement a follower – following system with an Abstractuser model.

I can update followers for each user profile in the backend which is reflected and shows the right button (follow / unfollow) on the users profile — so the logic is working.

However when I try to create my views.py file so that the user can follow / unfollow themselves nothing I’ve tried works (granted I know nothing) but if anyone can point me in the right direction would be greatly appreciated.

Here are snippets of my code;

# models.py

class NewUser(AbstractUser):
    
    birthdate = models.DateField(max_length=8, null=True, blank=True)
    portrait = models.ImageField(upload_to='portrait', default="../static/portrait/portrait_placeholder.png", null=True, blank=True)
    bio = models.TextField(verbose_name='Biography', max_length=600, null=True, blank=True)
    slug =  AutoSlugField(populate_from='first_name', unique_with='last_name')
    follows = models.ManyToManyField("self", related_name="followed_by", symmetrical=False, blank=True)

    def count_followers(self):
        return self.follows.count()
    
    def count_following(self):
        return NewUser.objects.filter(follows=self).count()

    def __str__(self):
        return self.username


class Product(models.Model):
    
    creator = models.ForeignKey(NewUser, on_delete=models.CASCADE, null=True, blank=True)
    name = models.CharField("Product Name", max_length=100, default="", help_text="This is the help text")
    concept = models.TextField(verbose_name='Concept', max_length=600, blank=True)
    created_date = models.DateTimeField(auto_now_add=True)
    updated_date = models.DateTimeField(auto_now=True)
    likes = models.ManyToManyField(NewUser, related_name='product_likes', blank=True)

    def total_likes (self):
        return self.likes.count()

    class Meta:
        ordering = ['-created_date']
# views.py

# view of populated profile detail view with associated profile products
class ProfileDetailView(DetailView):
    model = NewUser
    def get_context_data(self, **kwargs):
    # Call the base implementation first to get a context
        context = super().get_context_data(**kwargs)
        # Add in a QuerySet of all the users products
        context['object_list'] = Product.objects.filter(creator=self.object)
        return context


def follow_user(request, pk):
    follow_user = NewUser.objects.get(pk=request.user.id)

    if request.method == "POST":
        current_user_profile = request.user
        action = request.POST['follow']
        if action == "unfollow":
            current_user_profile.follows.remove(follow_user)
        elif action == "follow":
            current_user_profile.follows.add(follow_user)
        
    return render(request, 'authenticate/profile_detail.html', {"follow_user":follow_user})

# newuser_detail.html

<div class="row">
    <div class="col-md-auto p-3">
        <div class="button mt d-flex flex-row align-items-center">
            <a href="" class="h6">
                Following {{ newuser.count_followers }}
                <br>
                {% for username in newuser.follows.all %}
                    <a href="{% url 'profile' username.slug %}"> @{{ username }} </a><br/>
                {% endfor %}
        </div>
    </div>
    <div class="col-md-auto p-3">
        <div class="button mt d-flex flex-row align-items-center">
            <a href="" class="h6">
                Followers  {{ newuser.count_following }}
                <br>
                {% for username in newuser.followed_by.all %}
                    <a href="{% url 'profile' username.slug %}"> @{{ username }} </a><br/>
                {% endfor %}
        </div>
    </div>
  </div>

<form method=POST>
  {% csrf_token %}
  {% if newuser in user.follows.all %}
      <button class = "btn btn-outline-danger" name="follow" value="unfollow" type="submit">unfollow {{ newuser.first_name }}</button>
  {% else %}
      <button class = "btn btn-outline-success" name="follow" value="follow" type="submit">follow {{ newuser.first_name }}</button>
  {% endif %}
</form>

Can you provide more details around what exactly you’re doing, what you expect to happen and what is actually happening?

Yes sorry, I’m aiming to create a follow / following system similar to twitter or instagram. Each user has their own profile page, this includes portrait, bio, contact info etc, and products they’ve created — this part I’ve achieved.

A user can follow someone by clicking follow on their profile, updating their profile number of followers / following etc, same as twitter or instagram.

Additionally, I’d like to be able to create a page which shows only products from the users you follow.

Right now, it looks like the model is ok, and the logic in the html is working, when I add a follow through the admin panel, I see the correct button to either follow or unfollow a user on their profile.

However, when I click on the follow or unfollow button (same happens for both) I receive a HTTP ERROR 405 page in browser, instead of updating the follows model, and the error in terminal looks like this;
[01/Mar/2023 19:00:19] “POST /user/newuser HTTP/1.1” 405 0

An HTTP 405 is a “Method not allowed” error. You’re trying to POST to a URL that will only allow a GET.

Since your form tag doesn’t define an action, it’s going to try to post to the same view that rendered the original page. Is that the right URL?

ah, yes the idea is that when you press ‘follow’ it updates the model which is reflected in the button then showing ‘unfollow’ so it should be posting to the same view, not redirecting elsewhere, so it is the right url.

I did wonder if the slug in the newuser model is causing an issue, this is an example user profile page url; http://127.0.0.1:8000/user/thomas

What view is creating the page containing these buttons? Is it the ProfileDetailView?

What view is supposed to handle the “follow” processing, isn’t it follow_user?

yes that’s correct on both

So those are two different views - two different URLs. You need to define the action attribute of your form to submit to the url for follow_user, not ProfileDetailView.

ok, understood they’re two different URLS, I have the url for ‘follow’ here;

path('user/<slug>', views.follow_user, name='follow'),

and the profile detail view here;

path('user/<slug>', ProfileDetailView.as_view(), name='profile'),

I thought I was passing in the POST action through the views here, but maybe that is not correct.

#views.py 
def follow_user(request, pk, slug):
    follow_user = NewUser.objects.get(id=request.user.id)

    if request.method == "POST":
        current_user_profile = request.user.follow_user
        action = request.POST['follow']
        if action == "unfollow":
            current_user_profile.follows.remove(follow_user)
        elif action == "follow":
            current_user_profile.follows.add(follow_user)
    
    return render(request, 'follow', {"follow_user":follow_user})
    

If I change html to include;

<form action="{% url 'follow' username.slug %}" method="POST">

I then get:
Reverse for ‘follow’ with arguments ‘(’‘,)’ not found. 1 pattern(s) tried: [‘follow/(?P[^/]+)\Z’]

Those two are the same url. The second view (in order of being defined) will never be called. See URL dispatcher | Django documentation | Django

Since you’re calling ProfileDetailView both times, I’m going to assume that it actually appears before the other in your urls.py file.

In that situation, follow_user will never be called.

The name assigned to the url has nothing to do with the resolution of the request being received.

That error is telling you that username.slug has no value in the context at the time the template is being rendered. (And, even if you do get that right, the generated URL is still going to resolve to the other view.)