The ability to like/vote posts from the post list

I have a problem. I made likes to the posts on the video from YouTube. It is possible to like a post only from post_detail view and it's works correctly! How to make it possible to like posts in the post_list (in my case it's 'feed')? Please help!

MY CODE:

utils.py

def is_ajax(request):
    return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'

views.py

from .utils import is_ajax

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


def post_detail(request, slug):
    post = get_object_or_404(Post, slug=slug)
    comments = Comment.objects.filter(post=post, reply=None).order_by('-created_at')
    is_voted = False
    if post.votes.filter(id=request.user.id).exists():
        is_voted = True

    if request.method == 'POST':
        comment_form = CommentForm(request.POST or None)
        if comment_form.is_valid():
            content = request.POST.get('content')
            reply_id = request.POST.get('comment_id')
            comment_qs = None
            if reply_id:
                comment_qs = Comment.objects.get(id=reply_id)
            comment = Comment.objects.create(
                post = post,
                author = request.user,
                content = content,
                reply = comment_qs
            )
            comment.save()
            return redirect(post.get_absolute_url())
        else:
            for error in list(comment_form.errors.items()):
                messages.error(request, error)
    else:
        comment_form = CommentForm()
    return render(request, 'main/post_detail.html', {'post':post,
                                                     'comment_form':comment_form,
                                                     'comments':comments,
                                                     'is_voted':is_voted})


@login_required
def post_vote(request, slug):
    post = get_object_or_404(Post, id=request.POST.get('id'), slug=slug)
    is_voted = False
    if post.votes.filter(id=request.user.id).exists():
        post.votes.remove(request.user)
        is_voted = False
    else:
        post.votes.add(request.user)
        is_voted = True
    context = {
        'post': post,
        'is_voted': is_voted,
    }
    if is_ajax(request):
        html = render_to_string('main/votes.html', context, request)
        return JsonResponse({'form':html})

votes.html

<form action="{% url 'main:post-vote' slug=post.slug %}" method="post">
    {% csrf_token %}
    {% if is_voted %}
        <button type="submit" id="vote" name="post_id" value="{{ post.id }}" btn="btn btn-outline">Unvote</button>
    {% else %}
        <button type="submit" id="vote" name="post_id" value="{{ post.id }}" btn="btn btn-outline">Vote</button>
    {% endif %}
</form>
{{post.votes_count}}

post_detail.html


<div id="vote-section">
    {% include 'main/votes.html' %}
</div>

<script>
    $(document).ready(function(event){
        $(document).on('click','#vote', function(event){
            event.preventDefault();
            var pk = $(this).attr('value');
            $.ajax({
                type: 'POST',
                url: "{% url 'main:post-vote' slug=post.slug %}",
                data: {'id':pk, 'csrfmiddlewaretoken': '{{ csrf_token }}'},
                dataType: 'json',
                success: function(resoponse){
                    $('#vote-section').html(resoponse['form'])
                    console.log($('#vote-section').html(resoponse['form'])); 
                },
                error: function(rs, e){
                    console.log(rs.responseText);
                }
            })
        });
    });
</script>

feed.html

...
{% for post in posts %}
<div id="vote-section">
    {% include 'main/votes.html' %}
</div>
...
{% endfor %}

<script>
        $(document).ready(function(event){
            $(document).on('click','#vote', function(event){
                event.preventDefault();
                var pk = $(this).attr('value');
                $.ajax({
                    type: 'POST',
                    url: "{% url 'main:post-vote' slug=post.slug %}",
                    data: {'id':pk, 'csrfmiddlewaretoken': '{{ csrf_token }}'},
                    dataType: 'json',
                    success: function(resoponse){
                        $('#vote-section').html(resoponse['form'])
                        console.log($('#vote-section').html(resoponse['form'])); 
                    },
                    error: function(rs, e){
                        console.log(rs.responseText);
                    }
                })
            });
        });
</script>

models.py

class Post(models.Model):
    ...
    votes = models.ManyToManyField(get_user_model(), related_name='votes', blank=True)

    @property
    def votes_count(self):
        return self.votes.count()

urls.py

urlpatterns = [
    path('', views.feed, name='feed'),
    path('post_create/', views.post_create, name='post-create'),
    path('post/<slug>/', views.post_detail, name='post-detail'),
    path('post/<slug>/delete', views.post_delete, name='post-delete'),
    path('post/<slug>/update', views.post_update, name='post-update'),
    path('post/<slug>/vote', views.post_vote, name='post-vote'),
    path('comment/<slug>/delete', views.comment_delete, name='comment-delete'),
    path('comment/<slug>/update', views.comment_update, name='comment-update'),
    path('comment_reply/<slug>/delete', views.reply_comment_delete, name='comment-reply-delete')
]

I understand that I need to do the same as in post detail. I tried to do this, but i got errors

def post_detail(request, slug):
    post = get_object_or_404(Post, slug=slug)
    comments = Comment.objects.filter(post=post, reply=None).order_by('-created_at')
    is_voted = False
    if post.votes.filter(id=request.user.id).exists():
        is_voted = True
        ...

Tried to do:

def feed(request, slug=None):
    comments = Comment.objects.all()
    posts = Post.objects.all().order_by('-created_at')
    v_post = get_object_or_404(Post, slug=slug)
    is_voted = False
    if v_post.votes.filter(id=request.user.id).exists():
        is_voted = True
    return render(request, 'main/feed.html', {'posts': posts,
                                              'comments': comments,
                                              'is_voted': is_voted})

got

Page not found (404)

Also tried

def feed(request, slug):
    comments = Comment.objects.all()
    count_filter = Q(votes=request.user)
    vote_case = Count('votes', filter=count_filter, output_field=BooleanField())
    posts = Post.objects \
    .annotate(is_voted=vote_case) \
    .all().order_by('-created_at')
    return render(request, 'main/feed.html', {'posts': posts,
                                              'comments': comments})

got

TypeError at /
feed() missing 1 required positional argument: 'slug'

OK - there's a fair bit to unpack here:

First, generally speaking, it looks you you want Ajax to handle the bulk of the work. The reason (or at least the initial reason) why that isn't working above is in how you replicate forms

For well constructed HTML, only one element per page should have a particular ID

Your form is generated like this

{% for post in posts %}
<div id="vote-section"> #This will repeat for every post
    {% include 'main/votes.html' %}
</div>

What this means is that every form will have the same id, vote-section, so

success: function(resoponse){
                        $('#vote-section').html(resoponse['form'])

won't know where to write.

Similarly, in the forms on list, every button has id="vote"

This isn't an issue on the details page, because there's only one form and one vote/unvote button. On the list page, with multiple forms, it is.

So what to do about it?

First off, add a class to your button.

votes.html

<button type="submit" id="vote" class="vote-button" name="post_id" value="{{ post.id }}" btn="btn btn-outline">Unvote</button>

Next tweak your feed html so it uses IDs correctly. We'll use {{[forloop.counter][1]}} to generate unique IDs.

We'll also move the ajax into the loop, as the slug for each post is different in the url: generation section. We add the new div ID into the selector and the success function to make it unique.

feed.html

{% for post in posts %}
<div id="vote-section{{forloop.counter}}">
    {% include 'main/votes.html' %}
</div>
...

<script>
        $(document).ready(function(event){
            $(document).on('click','#vote-section{{forloop.counter}} .vote-button', function(event){
                event.preventDefault();
                var pk = $(this).attr('value');
                $.ajax({
                    type: 'POST',
                    //this needs to be in the for post in posts loop to get post.slug
                    url: "{% url 'main:post-vote' slug=post.slug %}",
                    data: {'id':pk, 'csrfmiddlewaretoken': '{{ csrf_token }}'},
                    dataType: 'json',
                    success: function(resoponse){
                        $('#vote-section{{forloop.counter').html(resoponse['form'])
                        console.log($('#vote-section{{forloop.counter}}').html(resoponse['form'])); 
                    },
                    error: function(rs, e){
                        console.log(rs.responseText);
                    }
                })
            });
        });
</script>


{% endfor %}

Now there are probably ways to make this more efficient, but hopefully it should get you over the hump to where it works.

Back to Top