Django + AJAX comments

Question:

I’m trying to implement a comment system without reloading the page, but it doesn’t work. Comments are inserted only after the page is reloaded, but AJAX does not work.

       <!-- COMMENT -->
    <div class="comment-section">
        <form action="{% url 'love:comment_urls' %}" method="POST" class='comment-form'>
            {% csrf_token %}
            <input type="hidden" name="post_id" value={{i.pk}}>
            {{ comment_form }}
            <button type="submit" name="submit_c_form">
                Опубликовать
            </button>
        </form>
    </div>
    <hr>

    <div class="comment_set">
        {% if i.quotes_comment.all %}
        {% for com in i.quotes_comment.all %}
        <b>{{ com.user }}:</b>
        {{ com.body }}
        {% endfor %}
        {% endif %}
    </div>
</div>

JS code

 // COMMENT 
$(document).ready(function () {
    $('.comment-form').submit(function () {
        event.preventDefault();
        console.log($(this).serialize());
        var url = $(this).attr('action')
        $.ajax({
            type: 'POST',
            url: url,
            data: $(this).serialize(),
            dataType: 'html',
            success: function (response) {
                console.log('Заработало')
                $('.comment-section').html(response['form']);
            },
            error: function (rs, error) {
                console.log(rs, error)

            }
        })

    })
})
@csrf_exempt
def create_comment(request):
profile = Profile.objects.get(user=request.user)

data = json.loads(request.body)
post_id = data['post_id']
body = data['body']

comment_form = CommentModelForm(data)

if comment_form.is_valid():
    print('Form is valid')
    instance = comment_form.save(commit=False)
    instance.user = profile
    instance.quotes = QuotesHonors.objects.get(
        id=post_id)
    instance.save()
    comment_form = CommentModelForm()

    return JsonResponse({
        'content': body
    })
    
return redirect("love:home_urls")

As I said, comments are inserted only after a reboot, but when I click publish, nothing happens. If you change the dataType to ‘json’, the parser error pops up. Can you explain what I’m doing wrong?

Asked By: h g

||

Answers:

Hard to say what you are doing wrong overall there is code missing. But from what you shared, csrf_token is not in AJAX request header. And mainly, by using JQuery .serialize(), you are trying to pass data in URL-encoded notation and not as a JSON object.

This .serialize() output would be used to pass data as a query string by appending it to a base URL. And, at the view accessing it with request.GET.get('parameter'). Instead, what we want is to pass data as JSON and access it through request.body at view level.

Here is a full example:

models.py

class Post(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    title = models.CharField(max_length=30)
    body = models.CharField(max_length=255)
    created_at = models.DateTimeField(auto_now_add=timezone.now())

class Comment(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments')
    body = models.CharField(max_length=255)
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments', blank=True)
    created_at = models.DateTimeField(auto_now_add=timezone.now())

urls.py

from .views import post_detail, comment_create

app_name = 'love'

urlpatterns = [
    path('post/<int:pk>/', post_detail, name='post-detail'),
    path('comment/create/', comment_create, name='comment-create'),
]

Two views, one to render the template and another to handle the AJAX request. The tricky part was trying to find a way to format date in the same way as the template.

views.py

from django.utils.formats import date_format
from django.shortcuts import get_object_or_404
from django.http import JsonResponse
import json

def post_detail(request, pk):
    post = get_object_or_404(Post, id=pk)
    has_commented = post.comments.filter(user=request.user).exists()

    context = {
        'post': post,
        'has_commented': has_commented
    }

    return render(request, 'post_detail.html', context)

def comment_create(request):
    data = json.loads(request.body)
    post_id = data['post_id']
    comment_body = data['body']
    
    post = get_object_or_404(Post, id=post_id)
    comment = Comment.objects.create(
        user=request.user, 
        body=comment_body, 
        post=post
    )

    created_at = date_format(comment.created_at, format='M. d, Y, h:m a', use_l10n=True)

    return JsonResponse({
        'user': comment.user.username, 
        'body': comment.body, 
        'created_at': created_at
        })

In this first template part we just display data, note that {# % if not has_commented % #} is commented out, when first writing the code I limited the number comments to one per User.

post_detail.html (HTML & DTL):

{% extends 'base.html' %}

{% block content %}
    <h1>{{post.title}}</h1>
    <h4>{{post.created_at}}</h4>
    <p>{{post.body}}</p>

    <hr>

    <div id="comment-div">
        {# % if not has_commented % #}
            <form action="{% url 'love:comment-create' %}" id="comment-form">
                <input type="hidden" id="post-id" value="{{post.id}}">
                <textarea id="comment-body" maxlength="255" rows="4" cols="50"></textarea>
                <br>
                <button type="submit" id="submit-comment">Comment</button>
            </form>
            <hr>
        {# % endif % #}
    </div>

    <div id="comment-list">
        <h2> Comments </h2>
        {% if post.comments.all %}
            {% for comment in post.comments.all %}
                <p>At {{comment.created_at|date:"M. d, Y, h:m a"}} {{comment.user}} commented:</p> 
                <p>{{comment.body}}</p>
            {% endfor %}
        {% endif %}
    </div>

This second part, containing the <script> is responsible for every event in the template after adding a new comment. The first function is used to retrieve csrftoken value from cookies.

Then, on submit click we collect data and send the AJAX request. Attention to a few lines. First, where csrf header name is set headers: {'X-CsrfToken': csrftoken}. Second, where data is converted to JSON string data: JSON.stringify({ post_id: post_id, body: body }).

post_detail.html (JS):

    <script>
        function getCookie(name) {
            let cookieValue = null;
            if (document.cookie && document.cookie !== '') {
                const cookies = document.cookie.split(';');
                for (let i = 0; i < cookies.length; i++) {
                    const cookie = cookies[i].trim();
                    // Does this cookie string begin with the name we want?
                    if (cookie.substring(0, name.length + 1) === (name + '=')) {
                        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                        break;
                    }
                }
            }
            return cookieValue;
        }

        $('#submit-comment').click( function (e) {
            e.preventDefault();
            var url = $('#comment-form').attr("action");
            var post_id = $('#post-id').attr("value");
            var body = $('#comment-body').val();
            const csrftoken = getCookie('csrftoken');

            $.ajax({
                method: "POST",
                url: url,
                headers: {'X-CsrfToken': csrftoken},
                data: JSON.stringify({ post_id: post_id, body: body }),
                success: function(comment) {
                    // $('#comment-div').prop("hidden", true);
                    $('#comment-body').val('');
                    $('#comment-list').append(
                        `<p>At ${comment.created_at} ${comment.user} commented:</p> 
                        <p>${comment.body}</p>`
                    );
                }
            });
        });
    </script>
{% endblock %}

Lastly, on success, clear body input and update the template by appending data to <div id="comment-list">. To limit to one comment per user, uncomment $('#comment-div').prop("hidden", true);

Answered By: Niko
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.