A little Hacker News in Django (part 9)

Daniel Dương
Jun 13, 2018 · 4 min read

Let’s add a way to reply to a comment. Today I want to start on the template/VueJS side.

First, let’s pass is_authenticated to our VueJS part.

var app = new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
post: {{ post_json|safe }},
is_authenticated: {% if user.is_authenticated %}true{% else %}false{% endif %},
},
methods: {
upvote: function (obj) {
$.post({
url: obj.upvote_url,
data: JSON.stringify({
upvoted: !obj.upvoted
}),
success: function (data, text_status, jq_XHR) {
obj.upvoted = !obj.upvoted
},
})
}
}
})

So then, instead of using Django template’s conditional I’m going to do this:

<div id="app">
[[ post.title ]] - [[ post.how_long_ago ]] - [[ post.domain_name ]]
<template v-if="is_authenticated">
- <span v-if="post.upvoted">Upvoted</span><span v-else>Not Upvoted</span>
- <button v-on:click="upvote(post)">Toggle upvoted</button>
</template>


<template v-if="is_authenticated">
<form action="" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit" />
</form>
</template>
<ul v-for="comment in post.comments">
<li>[[ comment.content ]] - [[ comment.how_long_ago ]]</li>
<template v-if="is_authenticated">
- <span v-if="comment.upvoted">Upvoted</span><span v-else>Not Upvoted</span>
- <button v-on:click="upvote(comment)">Toggle upvoted</button>
</template>
</ul>
</div>

Now, let’s write a component for the comments.

Vue.component('comment-comp', {
data: function () {
return {
show_reply: false
}
},
delimiters: ['[[', ']]'],
props: ['comment', 'is_authenticated'],
template: `<li>[[ comment.content ]] - [[ comment.how_long_ago ]]
<template v-if="is_authenticated">
- <span v-if="comment.upvoted">Upvoted</span><span v-else>Not Upvoted</span>
- <button v-on:click="send_upvote">Toggle upvoted</button>
</template></li>`,
methods: {
send_upvote: function () {
this.$emit('send_upvote');
}
}
});

Now our template looks like this:

<ul v-for="comment in post.comments">
<comment-comp v-bind:comment="comment" v-on:send_upvote="upvote(comment)" v-bind:is_authenticated="is_authenticated"></comment-comp>
</ul>

Now let’s add a textarea for a reply.

Vue.component('comment-comp', {
data: function () {
return {
show_reply: false,
content: '',
}
},
delimiters: ['[[', ']]'],
props: ['comment', 'is_authenticated'],
template: `<li>[[ comment.content ]] - [[ comment.how_long_ago ]]
<template v-if="is_authenticated">
- <span v-if="comment.upvoted">Upvoted</span><span v-else>Not Upvoted</span>
- <button v-on:click="clicked">Toggle upvoted</button>
</template>
<button v-on:click="show_textarea">Reply</button>
<template v-if="show_reply">
<textarea v-model='content'></textarea>
<button v-on:click="send_comment">Submit</button>
</template>
</li>`,
methods: {
clicked: function () {
this.$emit('clicked')
},
show_textarea: function() {
this.show_reply = true
},
send_comment: function() {
console.log(this.content)
}
}
});

The component is good, the comment is logged when I submit it. Let’s write a view to add a reply.

@require_POST
@login_required
def add_reply(request, comment_id):
comment = get_object_or_404(Comment, id=comment_id)
try:
content = json.loads(request.body.decode('utf-8'))['content']
except (json.JSONDecodeError, KeyError):
return HttpResponseBadRequest()
Comment.objects.create(content=content, creator=request.user,
post=comment.post, parent=comment)
return HttpResponse(status=204)

There are some things I’m not checking, like: What if the user sends an integer or a boolean, and empty string or null? But at this point, I don’t really care and I want to finish this thing even if it’s crap haha.

Now let’s add this view in our routes:

from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
from hnews.posts import views as posts_views

urlpatterns = [
path('admin/', admin.site.urls),
path('posts/', include('hnews.posts.urls')),
path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
path('comments/<int:comment_id>/set_upvoted/', posts_views.set_upvoted_comment, name='set_upvoted_comment'),
path('comments/<int:comment_id>/reply/', posts_views.add_reply, name='add_reply'),
]

Let’s change our Comment model a little bit:

def to_dict(self, user):
return {
'content': self.content,
'how_long_ago': self.how_long_ago(),
'creator': self.creator.username,
'upvoted': self.upvotes.filter(user=user).count() > 0,
'comment_url': reverse('add_reply', kwargs={'comment_id': self.id}),
'upvote_url': reverse('set_upvoted_comment', kwargs={'comment_id': self.id}),
'replies': [
reply.to_dict(user)
for reply in self.replies.all()
],
}

Let’s modify our Post model also to consider only the first level comments.

def to_dict(self, user):
return {
'title': self.title,
'how_long_ago': self.how_long_ago(),
'domain_name': self.get_domain_name(),
'creator': self.creator.username,
'upvoted': self.upvotes.filter(user=user).count() > 0,
'upvote_url': reverse('posts:set_upvoted_post', kwargs={'post_id': self.id}),
'comments': [comment.to_dict(user) for comment in self.comments.filter(parent=None)]
}

Let’s go back to our javascript. We need to modify our component to do recursive stuff in our template

template: `<li>[[ comment.content ]] - [[ comment.how_long_ago ]]
<template v-if="is_authenticated">
- <span v-if="comment.upvoted">Upvoted</span><span v-else>Not Upvoted</span>
- <button v-on:click="send_upvote">Toggle upvoted</button>
</template>
<button v-on:click="show_textarea">Reply</button>
<template v-if="show_reply">
<textarea v-model='content'></textarea>
<button v-on:click="send_comment">Submit</button>
</template>
<ul v-for="reply in comment.replies">
<comment-comp v-bind:comment="reply" v-bind:is_authenticated="is_authenticated"></comment-comp>
</ul>
</li>`,

Now let’s write the method to add a reply.

send_comment: function() {
$.post({
url: this.comment.comment_url,
data: JSON.stringify({
content: this.content
}),
success: function (data, text_status, jq_XHR) {
location.reload(true);
},
})
}

Ok… Reloading the page is kind of a hack, maybe I’ll fix that later.

If I go to http://localhost:8000/posts/1/, everything seems alright… Except the upvote. It works for the first level, but not the other levels. Let’s try to fix that.

First let’s pass the comment when we send_upvote .

template: `<li>[[ comment.content ]] - [[ comment.how_long_ago ]]
<template v-if="is_authenticated">
- <span v-if="comment.upvoted">Upvoted</span><span v-else>Not Upvoted</span>
- <button v-on:click="send_upvote(comment)">Toggle upvoted</button>
</template>
<button v-on:click="show_textarea">Reply</button>
<template v-if="show_reply">
<textarea v-model='content'></textarea>
<button v-on:click="send_comment">Submit</button>
</template>
<ul v-for="reply in comment.replies">
<comment-comp v-bind:comment="reply" v-on:send_upvote="send_upvote(reply)" v-bind:is_authenticated="is_authenticated"></comment-comp>
</ul>
</li>`,

Now send_upvote looks like this:

send_upvote: function (comment) {
this.$emit('send_upvote', comment)
},

And now the first level comments:

<ul v-for="comment in post.comments">
<comment-comp v-bind:comment="comment" v-on:send_upvote="upvote($event)" v-bind:is_authenticated="is_authenticated"></comment-comp>
</ul>

OK, now the hard things have been done. There is still some stuff to do, but let’s call it done 😛.

See ya!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store