Where to put form functions in View (Follow & Unfollow Buttons)

I’m trying to add a follow / unfollow button for user profiles. I’ve gone through several tutorials/forum posts which have gotten me this far, and I’m pretty sure my view is now where I’m going wrong the most. If helpful to note, I also suspect a bit of the problem is me not fully understanding how to convert and/or combine FBVs and CBVs because I started with a CBV and a lot of tutorials are using FBVs.

The button currently shows as follow/unfollow correctly on different user profiles. When the follow or unfollow button is clicked, it returns this error in the terminal:

Method Not Allowed (POST): /profiles/7429138d-699c-48c9-97f2-5698b59ff946
Method Not Allowed: /profiles/7429138d-699c-48c9-97f2-5698b59ff946
[22/Jun/2023 14:55:22] “POST /profiles/7429138d-699c-48c9-97f2-5698b59ff946 HTTP/1.1” 405 0

Questions

  1. I read that the HTTP 405 Method Not Allowed “indicates that the server knows the request method, but the target resource doesn’t support this method” — what is the “target resource”?
  2. In the view, should the # Trying to follow / unfollow profiles part be separated out into its own function within ProfileDetailView instead of part of get_context_data?
    2a. If yes to Q2, what should I be looking at to try to connect what’s in the template with that function of the view? Am I supposed to name the form?
    2b. If no to Q2, what am I supposed to try and add as context to be sent to the template?

Models

class Profile(models.Model):
    id = models.UUIDField( 
        primary_key=True,
        default=uuid.uuid4,
        editable=False)
    user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
    following = models.ManyToManyField(CustomUser, related_name="followed_by", symmetrical=False, blank=True)

class CustomUser(AbstractUser):
    pass

View

class ProfileDetailView(DetailView):
    model = Profile
    context_object_name = "profile"  
    template_name = "profiles/profile_detail.html" 

    def get_object(self, **kwargs):
        pk = self.kwargs.get('pk')
        view_profile = Profile.objects.get(pk=pk)
        return view_profile
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        view_profile = self.get_object()
        my_profile = Profile.objects.get(user=self.request.user)

        context["view_profile_user"] = view_profile.user

        # Giving the context to show follow / unfollow in the template button 
        if view_profile.user in my_profile.following.all():
            follow = True
        else:
            follow = False
        context["follow"] = follow

        # Trying to follow / unfollow profiles 
        if self.request.method=="POST":
            action = self.request.POST['follow_or_unfollow']
            if action == "unfollow":
                my_profile.follows.remove(view_profile)
            elif action == "follow":
                my_profile.follows.add(view_profile)
        my_profile.save()

        return context

Template

{% if user.is_authenticated %}
    {% if profile %}
    <div class="container">
        <div class="row">
            <div class="col-8">
                <h1>{{ profile.user.username }}'s Profile</h1>
                <form method="POST">
                    {% csrf_token %}
                    {% if follow %}
                        <button class="btn btn-outline-danger" name="follow_or_unfollow" value="unfollow" type="submit">unfollow</button>
                    {% else %}
                        <button class="btn btn-outline-success" name="follow_or_unfollow" value="follow" type="submit">follow</button>
                    {% endif %}
                </form>
            </div>
<!-- there's more HTML but I believe above is the only relevant excerpt  -->

Actually, after all that, I’m thinking maybe it’s because of the type of CBV (like it’s DetailView, but should be UpdateView since I’m trying to post something now as opposed to just a get request)?

I’ll try that out.

try get method with redirect view instead?

that bypasses the need for a form, etc and is I believe easier to manage… ?

also, can a non-authenticated user follow anyone? where/how do you protect yourself against it if not the case?

Okay, going from DetailView to UpdateView did help, here’s an update / new question:

Now the follow / unfollow button does not throw an error page. When an unfollow button is clicked, it wipes the entire list of “Followers” of that profile. When a follow button is clicked, it wipes the entire list of users “Following” that profile.

Updated Question
The above behavior leads me to believe I’ve got something wrong with the my_profile.following.remove(view_profile.user) and my_profile.following.add(view_profile.user) parts but I’m struggling to identify what’s happening. Is that the right place I should be looking to change?

Updated View (V2)
Note: same model + template

class ProfileDetailView(UpdateView):
    model = Profile
    context_object_name = "profile"  
    template_name = "profiles/profile_detail.html" 
    fields = ["following"]

    def get_object(self, **kwargs):
        pk = self.kwargs.get('pk')
        view_profile = Profile.objects.get(pk=pk)
        return view_profile
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        view_profile = self.get_object()
        my_profile = Profile.objects.get(user=self.request.user)

        context["view_profile_user"] = view_profile.user

        # Giving the context to show follow / unfollow in the template button 
        if view_profile.user in my_profile.following.all():
            follow = True
        else:
            follow = False
        context["follow"] = follow

        # Trying to follow / unfollow profiles 
        if self.request.method=="POST":
            action = self.request.POST['follow_or_unfollow']
            if action == "unfollow":
                my_profile.following.remove(view_profile.user)
            elif action == "follow":
                my_profile.following.add(view_profile.user)
            my_profile.following.save()

        return context

Thanks, plopidou - I’m not sure I know enough to understand what you’re recommending but will explore it.

In the template, I have {% if user.is_authenticated %} set up so that a non-authenticated user wouldn’t even see the profile pages (it would instead show them options to log in or sign up).

is your view protected too?

simply hiding the link/button is not enough. Your view/python code needs to be protected too.

if not someone can paste /path/to/following/ without being authenticated and see what happens… ?

Then I suppose it’s not yet, though I feel compelled to mention this is not a deployed project so there’s still much to be done and I’m still learning. I’ll get there :sweat_smile:

Might you point me in the right direction on the question:

Still actively trying to figure this out, if anyone might be able to point me in the right direction; thanks so much

Please confirm that this is your form that you’re rendering and submitting to follow or unfollow a profile:

You’re not passing the profile to be followed or unfollowed in the form being submitted. The view has no way to know which profile is to be acted upon.

Side note: I wouldn’t be processing a POST in a detail view. I wouldn’t have this form submit to the detail view. I’d set up a separate view to handle the follow / unfollow requests - this action really doesn’t belong with the detail view.

Yes, confirmed.

Oh, I thought the view_profile variable in ProfileDetailView was what told the view which profile to follow/unfollow. I’ll try to read through again and understand where I went wrong.

I’d be happy to try this, I’m just confused on how to make the switch, especially the URL part of it (since the follow/unfollow button is on the profile detail page, and I’ve read elsewhere that you can’t have more than one view per URL, that’s why I put it there - is it correct that it’s 1:1 for a view and a URL?). Maybe relatedly, plopidou earlier mentioned using a redirect view to bypass the need for a form (I couldn’t figure out how to do this), but it sounds like you’re saying it’s okay to use the form if I create a separate, second view (probably using FormView I’m assuming?) for it outside of the ProfileDetailView. Is that correct?

Also I didn’t include my url pattern in my first post, so adding it here just in case it’s helpful:

urlpatterns = [
    path("<uuid:pk>/", ProfileDetailView.as_view(), name="profile_detail"), 
]

PS: I saw a little cake icon by your name, so I’m assuming it’s your birthday - Happy Birthday! Thanks for giving me the gift of a knowledge on YOUR birthday :gift: :disguised_face:

You didn’t - I didn’t quite follow this through far enough. I stand corrected.

The “action” attribute on the form tag defines the URL to which the form would be submitted. See <form>: The Form element - HTML: HyperText Markup Language | MDN

So it would be a different view with a different url.

This means that your current view doesn’t change - you only need to add the action attribute to the form tag in your template and add the new view.

The new view can then use the ProfileDetailView as its success url to redirect back to that view.

Yep, I’m now legal drinking age, times 3. :slight_smile:

To clarify, I originally started this post with using Django’s DetailView for ProfileDetailView (thus the name), but I realized it wouldn’t work and changed it to using Django’s UpdateView later in this post:

(Obviously the naming isn’t great so I’ll plan to update it to make more sense once it’s working.)

Anyway, I started to try what you described, but to clarify, is this what you’re recommending:

  • switch ProfileDetailView back to using Django’s DetailView instead of UpdateView * (I’m unsure about this since you said the “current view doesn’t change" but I’m wondering if maybe my poor naming played a part)
  • add a separate view, maybe titled something like ProfileFollowButton, which uses Django’s FormView (or maybe UpdateView?)
  • remove the # Trying to follow / unfollow profiles section of the code from ProfileDetailView and add it to the new view ProfileFollowButton (again, I’m unsure about this since you said the “current view doesn’t change” - but otherwise I’m not sure what I’d put in the new view)
  • include a success URL in ProfileFollowButton so it redirects back to ProfileDetailView
  • update the template so the form part has <form method="POST" action="{% url 'url_name_here' %}">

That’s three beers on you then lol cheers :beers:

Correct, I missed that change from the original post to the subsequent reply.

Personally, I’d make it an FBV, since you’re not going to be using it to render any forms. It has no need to respond to a GET, so the processing for it can be very simple.

Poor choice of words (insufficient clarity) on my part.

Yes, the get_context_data method could be simplified, and probably should. But, technically, since this view will no longer receive a POST request, you don’t need to remove that block of code. ("self.request.method=="POST" will always be False, and so never executed.)

You’re going to want to pass the current object id as a parameter to that new view, and so the url tag will probably look more like:
{% url 'url_name_here' view_profile.id %}

Thanks. Below is an updated version that I think is the closest I’ve tried (not working yet):

Template

{% if user.is_authenticated %}
    {% if profile %}
    <div class="container">
        <div class="row">
            <div class="col-8">
                <h1>{{ profile.user.username }}'s Profile</h1>
                <form method="POST" action="{% url 'profile_follow_feature' view_profile.id %}>
                    {% csrf_token %}
                    {% if follow %}
                        <button class="btn btn-outline-danger" name="follow_or_unfollow" value="unfollow" type="submit">unfollow</button>
                    {% else %}
                        <button class="btn btn-outline-success" name="follow_or_unfollow" value="follow" type="submit">follow</button>
                    {% endif %}
                </form>
            </div>
<!-- there's more HTML but I believe above is the only relevant excerpt  -->

urls

urlpatterns = [
    path("<uuid:pk>/", ProfileDetailView.as_view(), name="profile_detail"), 
    path("<uuid:pk>/profilefollowbutton", profile_follow_button, name="profile_follow_feature"), 
]

views

class ProfileDetailView(DetailView):
    model = Profile
    context_object_name = "profile"  
    template_name = "profiles/profile_detail.html" 

    def get_object(self, **kwargs):
        pk = self.kwargs.get('pk')
        view_profile = Profile.objects.get(pk=pk)
        return view_profile
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        view_profile = self.get_object()
        my_profile = Profile.objects.get(user=self.request.user)

        context["view_profile_user"] = view_profile.user

        # Giving the context to show follow / unfollow in the template button 
        if view_profile.user in my_profile.following.all():
            follow = True
        else:
            follow = False
        context["follow"] = follow

        return context

# FBV to process a click of the follow button
# # Trying to follow / unfollow profiles 
def profile_follow_button(request, view_profile):
    if request.method == "POST":
        action = request.POST['follow_or_unfollow']
        if action == "unfollow":
            request.user.profile.following.remove(view_profile.id)
        elif action == "follow":
            request.user.profile.following.add(view_profile.id)
        request.user.save()
        return render(request, "profiles/profile_detail.html")

It seems like I have a problem with what I put for the form action: {% url 'profile_follow_feature' view_profile.id %} because when I try to go to any profile detail page, there’s an error of

NoReverseMatch at /profiles/f903466a-4761-4a1c-91ef-88a3c89d446c/
Reverse for ‘profile_follow_feature’ with arguments ‘(’‘,)’ not found. 1 pattern(s) tried: [‘profiles/(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/profilefollowbutton\Z’]

Among other things, I tried switching to action="{% url 'profile_follow_feature' view_profile.pk %} (id → pk)

Also, I think you meant something different when you said to give a success url since you said, in this case, it’s “[not used] to render any forms”, but I know something like success_url = reverse('profile_detail') is for CBVs and not FBVs and couldn’t find something better.

Finally, I’m unsure if I can write the FBV profile_follow_button like I did. For example, is passing in the view_profile at the top like I did a way to do that successfully?

This error:

Is telling you that you’re not actually passing a parameter to the reverse function being used by the url tag. That means that in your template, the variable name is not correct.

This change:

is not material because pk is a synonym for whatever field is being used as the Primary Key for the model. In this particular case, the PK for Profile is named id.

In your template:

needs to match what you’re actually passing in the context which is this:

Yes, in implementation - but the same basic logic.

In the typical pattern of form processing (see the example for a view in the Working with Forms docs) when you process a form, you have three cases to deal with:

  • GET
  • POST with invalid data
  • POST with valid data.

As discussed earlier, you can ignore or reject a GET as you see fit. You can reject it outright, or you can do something else like a redirect. That choice is entirely up to you. (You still want to do something for it - I’ll say a little more about this later.)

Your other two cases, valid and invalid data, come down to whether your get the value “follow” or “unfollow” for the follow_or_unfollow field in the POST data, or something else (or nothing).

(How can you get something else? Easy - either someone runs a crawler on your site and discovers that url, or someone opens their browser’s developer tools and changes the form. You must always think of the cases where you may not get what you’re expecting. You must never trust the data coming from the browser to always be “right”, correct, or appropriate.)

You’re actually doing this with your if / elif conditional block, so that’s good.

However, what you don’t need here is a condition for if request.method. If you use one of the method decorators as mentioned above, this view will only ever get a POST. There’s no need to check it again.

If you don’t use the decorator, then changing the next line to action = request.POST.get('follow_or_unfollow') will still handle the GET case by setting action to None, which means that neither condition will be true and nothing will happen to the profile. (This is where I was going with my continuation from earlier.)

Side note: You may also wish to perform a check in the post to see if it’s the right value being submitted. What’s going to happen if a person follows twice? Or if they unfollow someone they’re not following?

Next point is that since the changes to a ManyToMany field do not actually change either of the models involved, there’s no need for the request.user.save function. You’re not changing the user model.

Next, as shown in the Working with Forms example referenced above, at the end of processing a form with a POST, the common process is to redirect to a page, not to render a page. You want to redirect to the profile_detail view, passing that view_profile as a parameter.

Finally, the following field in the Profile model is an M2M that references the CustomUser object, not a Profile. However, what you’re passing to the view as the parameter is the pk of a Profile. This means that your view needs to use that pk to retrieve the CustomUser object associated with that referenced Profile, and assign it to the following M2M field.

1 Like

After some confusion and scrapping what I previously had to start over, I have it working - updated code:
urls

urlpatterns = [
    path("<uuid:pk>/", ProfileDetailView.as_view(), name="profile_detail"), 
    path("<uuid:pk>/follow/", follow, name="follow"), 
    path("<uuid:pk>/unfollow/", unfollow, name="unfollow"), 
]

views

class ProfileDetailView(DetailView):
    model = Profile
    context_object_name = "profile"  
    template_name = "profiles/profile_detail.html" 

    def get_object(self, **kwargs):
        pk = self.kwargs.get('pk')
        view_profile = Profile.objects.get(pk=pk)
        return view_profile
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        view_profile = self.get_object()
        my_profile = Profile.objects.get(user=self.request.user)

        # context for following / followers lists
        context["view_profile_user"] = view_profile.user

        # context for showing follow / unfollow button at all
        if view_profile == my_profile:
            is_me = True
        else:
            is_me = False
        context["is_me"] = is_me

        # context for knowing to show follow or unfollow in template button
        if view_profile.user in my_profile.following.all():
            is_following = True
        else:
            is_following = False
        context["is_following"] = is_following

        return context

def follow(request, pk):
    view_profile = Profile.objects.get(pk=pk)
    request.user.profile.following.add(view_profile.user)
    return HttpResponseRedirect(reverse("profile_detail", args=(pk,)))

def unfollow(request, pk):
    view_profile = Profile.objects.get(pk=pk)
    request.user.profile.following.remove(view_profile.user)
    return HttpResponseRedirect(reverse("profile_detail", args=(pk,)))

template

                <h1>{{ profile.user.username }}'s Profile</h1>
                {% if is_me %}
                {% else %}
                    {% if is_following %}
                        <form action="{% url 'unfollow' profile.pk %}" method="post">
                            {% csrf_token %}
                            <input type="submit" value="unfollow">
                        </form>
                    {% else %}
                        <form action="{% url 'follow' profile.pk %}" method="post">
                            {% csrf_token %}
                            <input type="submit" value="follow">
                        </form>
                    {% endif %}
                {% endif %}
<!-- template continues before/after but above is the relevant excerpt -->

With this update, I wanted to ask more about the following three topics previously mentioned:

  1. covering the cases of valid and invalid data
  2. checking that the value submitted is the right value
  3. checking for authentication in the view

1) covering the cases of valid and invalid data

In the old code, you mentioned that “[I was] actually doing this with [my] if / elif conditional block” but because I changed the way the code was structured, I wanted to understand if the updated code was lacking in this area. My main question is:

  • do I need to do something else here to ensure I’ve covered the cases of valid/invalid data? (Because it’s now set up so there’s no follow_or_unfollow field like there used to be, I am unsure if anything else is needed.)

2) checking that the value submitted is the right value

Similar to my first question above, since I’m no longer sending a value through a button from the template into the view with the follow_or_unfollow field, I thought this recommendation might no longer applicable, however then if someone went directly to the follow or unfollow url paths, then I suppose it would mean there’s a case where someone could attempt to follow someone twice or unfollow someone they’re not following. What’s the typical way folks protect or check against that?

  • I’m thinking I’d probably put something right before the request.user.profile.following.add(view_profile.user) and request.user.profile.following.remove(view_profile.user) lines of code in follow() and unfollow() - is that correct?
  • If yes, what kind of thing would I actually be putting there?

3) checking for authentication in the view

For this, I’m just not sure how to authenticate someone in a view in addition to doing so in the template. Can you please point me in the right direction? I was struggling to find it in the docs, but this seems common, so I feel like it’s probably there somewhere and I’m just missing it.

No, because you’re dispaching to a view based on the url. The url is either going to have follow or unfollow if it’s going to call one of those two views. Anything else in that part of the url is going to generate a 404.

That depends upon whether or not you really care whether or not they do that.

I’m not in a position tonight to actually check this, but I don’t believe you need to worry about that. I know that if you add a relationship in that situation it doesn’t create a duplicate entry (See https://docs.djangoproject.com/en/4.2/topics/db/examples/many_to_many/#many-to-many-relationships). Trying to remove an entry that doesn’t exist shouldn’t create an error either.

Review the docs at Authentication in web requests

Got it :+1: Thanks for all the help!

1 Like