A little Hacker News in Django (part 9)

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!