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?
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);
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?
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);