How to use model.objects methods inside templates?

I want to check if the post is liked/not by the current user. If it is liked, then there should be a filled heart, otherwise empty heart. What I am trying to do is to implement a typical ‘like’ functionality of Instagram. I am trying to approach in this way, that on reloading the page, every post gets updated and their ‘like’ button as well according to as they have liked it or not. Then, if a user clicks on a like button, then in frontend ‘like’ button will change without referring to the backend. At the same time, the request to change the like in the backend has already been sent by ajax. How do I achieve this? And is there an even better way to do this?

feed.html:-

{% extends "user/base.html" %}
{% load static %}

{% block content %}
<div class="flex my-8 justify-center">
    <h2 class="text-4xl">All Posts</h2>
</div>

<div class="grid grid-cols-1">
    {% for post in posts %}
    <div class="flex justify-center">
        <div class="mb-20 md:mx-60 lg:mx-80">
            <div class="flex">
                <a href="#">
                    <div class="user-container h-12 w-12 mb-2 flex">
                        {% if post.user.profile.image %}
                        <img class="rounded-full" src="{{post.user.profile.image.url}}" alt="">
                        {% endif %}
                        <div class="px-3 flex items-center italic"><strong>{{post.user}}</strong></div>
                    </div>
                </a>
            </div>
            <div class="rounded-md overflow-hidden">
                <img class="h-96 w-96 shadow-lg" src={{post.image.url}} alt="">
            </div>
            <div>
                <div class="icon-container mt-1 flex items-center gap-3">
                    <a class="btn-like" id={{post.id}}>
                        {% if post.liked_by.filter(id=request.user.id).exists() %}
                        <img class="w-8 h-8" src={% static 'posts/heart.svg' %} alt="">
                        {% else %}
                        <img class="w-8 h-8" src={% static 'posts/heart-filled.svg' %} alt="">
                        {% endif %}
                    </a>
                    <img class="w-7 h-7" src={% static 'posts/comment.svg' %} alt="">
                    <img class="w-8 h-8" src={% static 'posts/share.svg' %} alt="">
                </div>
            </div>
            <div class="break-word w-96">
                <strong class="text-lg">{{post.title}}</strong>
                <p>{{post.caption}}</p>
            </div>
        </div>
    </div>
    {% endfor %}
</div>

<script type="text/javascript">
    window.CSRF_TOKEN = "{{csrf_token}}"
    $('.btn-like').on('click', function () {
        let post_id = parseInt(this.id);
        $('#' + post_id).children('img').attr('src', function (index, attr) {
            return attr == '/static/posts/heart.svg' ? null : '/static/posts/heart.svg';
        });
        $.ajax({
            method: "POST",
            url: "/posts/like/",
            data: {
                post_id: post_id,
                csrfmiddlewaretoken: window.CSRF_TOKEN
            }
        })
    })
</script>
{% endblock content %}

views.py:-

from django.shortcuts import render, get_object_or_404, redirect
from .forms import PostForm
from django.contrib.auth.decorators import login_required
from .models import Post

# Create your views here.
@login_required
def create_post(request):
    if request.method == 'POST':
        form = PostForm(data=request.POST, files=request.FILES)
        if form.is_valid():
            new_item = form.save(commit=False)
            new_item.user = request.user
            new_item.save()
    else:
        form = PostForm()
    return render(request, 'posts/create.html', {'form': form})
            
@login_required
def my_posts(request):
    current_user = request.user
    posts = Post.objects.filter(user=current_user)
    profile = current_user.profile
    return render(request, 'user/index.html', {'posts': posts, 'profile': profile})

def feed(request):
    posts = Post.objects.all().order_by('-created')
    return render(request, 'posts/feed.html', {'posts': posts})

@login_required
def like_post(request):
    post_id = request.POST.get('post_id')
    post = get_object_or_404(Post, id=post_id)
    if post.liked_by.filter(id=request.user.id).exists():
        post.liked_by.remove(request.user)
    else:
        post.liked_by.add(request.user)
    return redirect('feed')

You don’t want to do this. (Aside from the fact that it’s syntactically invalid - you can’t pass parameters to function calls in template variable references.)

Ideally, what you would do is annotate your posts query in your view with an attributed that indicates whether the current user has liked it.

I tried it but it gives me a post 2 times if the post is liked by current user.

def feed(request):
    posts = Post.objects.annotate(
        liked_by_current_user=Case(
            When(liked_by=request.user, then=True),
            default=False,
            output_field=BooleanField()
        )
    ).order_by('-created').distinct()
    return render(request, 'posts/feed.html', {'posts': posts})
                        {% if post.liked_by_current_user %}
                        <img class="w-8 h-8" src={% static 'posts/heart_filled.svg' %} alt="">
                        {% else %}
                        <img class="w-8 h-8" src={% static 'posts/heart.svg' %} alt="">
                        {% endif %}

By the way, I found another way:-

{% if user in post.liked_by.all %}
<img class="w-8 h-8" src={% static 'posts/heart_filled.svg' %} alt="">
{% else %}
<img class="w-8 h-8" src={% static 'posts/heart.svg' %} alt="">
{% endif %}

Still, please tell me the annotate way too.

The issue with doing this:

is that you’re still creating an N + 1 query situation.

For me to directly suggest an annotation, I’d need to see the models.

But why is whole post rendered 2 times? {% if user in post.liked_by.all %} is only for the like icon.
image

Here is my post model:-

class Post(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    image = models.ImageField(upload_to='images/%y/%m/%d')
    caption = models.CharField(max_length=500, blank=True)
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, blank=True)
    created = models.DateTimeField(auto_now_add=True)
    liked_by = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='posts_liked', blank=True)

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

The reason you are getting duplicates is because of the many_to_many relationship between Post and User, and with the annotation and with distinct, you’re getting two copies - one with liked_by_current_user=True and the other with liked_by_current_user=False

There are at least two different ways you can write this annotation.
1.

posts = Post.objects.annotate(
    liked_by_current_user=Count(
        'liked_by', filter=Q(liked_by=request.user)
    )
)

This returns a 1 or a 0 depending upon whether the current user likes that post.

posts = Post.objects.annotate(
    liked_by_current_user=Exists(
        Post.objects.filter(id=OuterRef('id'), liked_by=request.user)
    )
)

This returns a boolean value.

Note: I believe the second version here would be faster for larger tables (the efficiency of exists over count), but I’m not seeing any difference between the two in my test environment.

It works. Thanks.
I am just curious that this also works. Is it inefficient? :-

Yes, I answered that above at How to use model.objects methods inside templates? - #4 by KenWhitesell