Hacker News Clone – Django Project Idea

Hacker News

In this tutorial, we will build a Full Web App – similar to the Hacker News website using the Django Web Framework. This is a great Django project idea if you want to create a complete website. There is nothing better way to learn any framework than developing it yourself.

About Hacker News

Hacker News is a social news website run and handled by the Investment fund and start-up incubator Y-Combinator. This website mainly focusses on Computer Science and Entrepreneurship.

The website defines itself as a platform where one can share anything that “gratifies one’s intellectual curiosity.”

Have a look at the website here – Hacker News

We will make a web application consisting of all the main features of the website. Enough with the talking; let us now dive into it !!

Some Interesting Features of the Website

Now we will see the interesting features of the website that we will create using Django

1. The Top Navigation Bar

NavBar
HackerNews NavBar
  • The Hacker News button brings you back to the home page.
  • The New button shows all the latest submissions.
  • The Past Button shows you the submissions that were made 30 minutes before etc.
  • Similarly, there are ask, show, and jobs that are not so important.
  • Then there is a submit option and a log out/login option

We will code all these in our app.

2. Individual as well as list of Posts

Then we have a list of posts displayed on the main page.

List Of Posts
List Of Posts
  • Each Post has an Upvote option to vote the post
  • Each Post displays Total Votes and Total Comments on them
  • Displays the Username of the Creator
  • Displays the Submission Time

Also when you click on comments, the site redirects you to the Comments page:

Post
Post

Here we can post comments to the post, and we can reply to others as well.

Again an Interesting Feature here is to form the Threaded Comments.

That is when we reply to a comment; our reply should be shown right below it. It is not as easy as it sounds, don’t worry, we will look at that in the upcoming sections.

3. User Authentication

One more important feature is User Authentication. On the website, we can Post, Comment, or reply only when we have an account.

Signin
Signin

And the Sign-up View

Sign Up
Sign Up

We will again include both these views in our code !!

4. Post Submit View

The site has a submit View:

Submit
Submit

Here we can submit the Title, URL, and Description for the post. So that’s it guys !! This is what we have to do. So let’s get started !!

Coding the Hacker News Clone in Django Web Application

First, we have to create a new Django Project. So let’s do it:

django-admin startproject HackerNews

Also, Create the Django App. I have named mine – hnapp . So the code would be:

django-admin startapp hnapp

Nice. Don’t forget to add the app name in the settings.py file. Also, we will use Template Inheritance to code our templates.

Create a templates folder inside the Django HackerNews project directory and mention the path to it in the settings.py file.

We do it by adding the following line in TEMPLATES/settings.py

'DIRS': [os.path.join(BASE_DIR,'HackerNews/templates')],
Templates
Templates

Okay, Cool, now add the base.html – Our Base Template for the website, in the templates folder created:

Base
Base

In the File, add the code:

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<style>
body {
  margin: 0;
  font-family: Arial, Helvetica, sans-serif;
}

.topnav {
  overflow: hidden;
  background-color: #333;
}

.topnav a {
  float: left;
  color: #f2f2f2;
  text-align: center;
  padding: 14px 16px;
  text-decoration: none;
  font-size: 17px;
}

.topnav a:hover {
  background-color: #ddd;
  color: black;
}

.topnav a.active {
  background-color:	#FF0000;
  color: white;
}

.topnav-right {
  float: right;
}

p.ex1 {
  padding-left: 40px;
}

</style>
</head>
<body>
    {% block content %}
    {% endblock %}
</body>
</html>

This code is for the aesthetics of our web app. I’ve tried to add CSS to make it look better than the default layout.

Also in the Project/urls.py add the line:

from django.contrib import admin
from django.urls import path,include
urlpatterns = [
    path('admin/', admin.site.urls),
    path('',include('hnapp.urls')),
]

With that in place, let’s now move on to real Django Part.

1. Coding the Required Models

For the website, we need:

  • Post Model: To store the Post information
  • Vote Model: To store the Upvotes of each post
  • Comment Model: To store the comments on each post.

And the pre-built User Model to store User Account Information. So in the models.py add the following models:

Post Model:

class Post(models.Model):
    title = models.CharField("HeadLine", max_length=256, unique=True)
    creator = models.ForeignKey(User, on_delete= models.SET_NULL, null=True)
    created_on = models.DateTimeField(auto_now_add=True)
    url = models.URLField("URL", max_length=256,blank=True)
    description = models.TextField("Description", blank=True)
    votes = models.IntegerField(null=True)
    comments = models.IntegerField(null=True)    

    def __unicode__(self):
        return self.title

    def count_votes(self):
        self.votes = Vote.objects.filter(post = self).count()
    
    def count_comments(self):
        self.comments = Comment.objects.filter(post = self).count()

Here, we have two functions to count the total votes and total comments on each post. Note that the post shouldn’t get deleted when the creator deletes his account, hence set on_delete to models.SET_NULL.

The Vote Model:

class Vote(models.Model):
    voter = models.ForeignKey(User, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)

    def __unicode__(self):
        return f"{self.user.username} upvoted {self.link.title}" 

This model will store the information about which user has upvoted which post.

And the final Comment Model:

class Comment(models.Model):
    creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    content = models.TextField()
    identifier = models.IntegerField()
    parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True)

    def __unicode__(self):
        return f"Comment by {self.user.username}"

Each Comment will have a creator, the post on which the creator commented, and the comment content itself.

Now, each reply comment will also have a parent comment, i.e., the comment to which the reply was given. Hence we need a parent field, which is a foreign key to the comment model itself

We also need another field, an identifier field, to identify reply comments of different levels. To understand this, consider the following image:

Comment Threads
Comment Threads

Hence,

  • The comments made on the post itself will have identifier = 0 and parent = None since they are the topmost comment.
  • The reply comments at the first level will have identifier = 1, and the comment to which the reply was given as the parent comment (identifier=0)
  • Similarly, a reply comment on the second level will have identifier = 2 and a parent comment with identifier = 1.

We will see later, how we will use these two fields to display the comments in the threaded fashion.

That’s it with the models, now register the three models in the admin.py using the admin.site.register(model_name) line.

Also Don’t forget to run the migrations using the code:

python manage.py migrate
python manage.py makemigrations
python manage.py migrate

2. Coding the Views and corresponding Templates

With the models in place, let us now code the views. Now, for the complete website we require the following view:

  1. Home Page View: To Display the list of Posts
  2. New Posts View: To Display all the latest Posts
  3. Past Posts View: To Display the Posts that are 30 or more minutes old
  4. Single Post View: To Display the Comment Form and the existing Comments
  5. Reply-Comment View: To display the Form for replying existing comments
  6. User Info View: To Display the Information Regarding a user
  7. User Posts View: To Display the posts of a particular user
  8. Submit View: To Display the Submit Form
  9. Edit view: To Edit the submitted Form
  10. Sign in View: To Display the Sign-in Page
  11. Sign-up View: To Display the Sign-up Page
  12. Sign-out View: To log the user out

Apart from them, we need two more Views to handle UpVoting and DownVoting of posts.

Wow lot’s of views !! So let’s get started without wasting much time.

1. Home Page View

So in the Views.py, add the PostListView (Home page) function view:

def PostListView(request):
    posts = Post.objects.all()
    for post in posts:
        post.count_votes()
        post.count_comments()
        
    context = {
        'posts': posts,
    }
    return render(request,'postlist.html',context)

For each post, we count the total votes and comments before displaying them on the webpage.

The url endpoint in urls.py:

path('',PostListView, name='home'),

Add the postlist.html Template in a templates folder in the Django App folder itself.

{% extends 'base.html' %}
{% block content %}

<div class="topnav">
  <a class="active" href="{% url 'home'%}">Hacker News</a>
  <a href="{% url 'new_home'%}">New</a>
  <a href="{% url 'past_home'%}">Past</a>
  <a href="{% url 'submit'%}">Submit</a>

  {% if request.user.is_authenticated %}
    <div class="topnav-right">
      <a href="{% url 'signout' %}">Sign Out </a>
    </div>
  {% else %}
    <div class="topnav-right">
      <a href="{% url 'signin' %}">Sign In </a>
    </div>
  {% endif %}
</div>

<div class="w3-panel w3-light-grey w3-leftbar w3-border-grey">
  <ol>
{% for post in posts %}
  
  <li><p><a href = "{{post.url}}"><strong>{{post.title}}</strong></a> - <a href = "{% url 'vote' post.id %}">Upvote</a> - <a href = "{% url 'dvote' post.id %}">Downvote</a></p>
  
  {% if post.creator == request.user%}
    <p>{{post.votes}} votes | Created {{post.created_on}}| <a href = "{% url 'user_info' post.creator.username %}">{{post.creator.username}}</a> | <a href="{% url 'post' post.id %}"> {{post.comments}} Comments</a> | <a href="{% url 'edit' post.id %}"> Edit</a></p></li>
  {%else %}
    <p>{{post.votes}} votes | Created {{post.created_on}}| <a href = "{% url 'user_info' post.creator.username %}">{{post.creator.username}}</a> | <a href="{% url 'post' post.id %}"> {{post.comments}} Comments</a></p></li>
  {%endif%}

{% endfor %}
</ol>
</div>

{% endblock %}

Here, we will show sign-out in the NavBar if the user is logged in and Sign-in if the user is not.

For Each Post, we will show the Post’s Title, Creator, Creation Date and Time, Total Votes, and Comments.

Also, if the post’s creator is the user itself, then we will display an edit option as well.

2. New and the Past Posts View

The New View will show all the latest Posts first. So the Code to do that will be:

def NewPostListView(request):
    posts = Post.objects.all().order_by('-created_on')
    for post in posts:
        post.count_votes()
        post.count_comments()    
    context = {
        'posts': posts,
    }
    return render(request,'hnapp/postlist.html', context)

Similarly, to show the Posts created 30 or more minutes before, we use the DateTime library of Python. Hence the code would be:

from datetime import datetime,timedelta
from django.utils import timezone

def PastPostListView(request):
    time = str((datetime.now(tz=timezone.utc) - timedelta(minutes=30)))
    posts = Post.objects.filter(created_on__lte = time)
    for post in posts:
        post.count_votes()
        post.count_comments()

    context={
        'posts': posts,
    }
    return render(request,'hnapp/postlist.html',context)

Here the __lte function stands for less than or equal. Therefore, it filters out all the posts with created_on time less than the time that was half an hour ago.

The url endpoints for the views:

path('new',NewPostListView, name='new_home'),
path('past',PastPostListView, name='past_home'),

The template for these two will be the same as the Home Page View.

3. User Info and User Posts View

When a client clicks on the post’s creator name, he should reach the User Info Page.

The User Info page should display the username, creation date, and a link to a page showing the User Posts.

So let’s code both the user info and user posts view here:

def UserInfoView(request,username):
    user = User.objects.get(username=username)
    context = {'user':user,}
    return render(request,'user_info.html',context)


def UserSubmissions(request,username):
    user = User.objects.get(username=username)
    posts = Post.objects.filter(creator = user)
    for post in posts:
        post.count_votes()
        post.count_comments()    
    return render(request,'user_posts.html',{'posts': posts})

In the UserSubmissions view, before displaying the posts, we calculate the total votes and comments using the for loop.

The URL endpoints for the views are:

path('user/<username>', UserInfoView, name='user_info'),
path('posts/<username>',UserSubmissions, name='user_posts'),

And the corresponding templates will be:

user_info.html:

{% extends 'base.html' %}
{% block content %}

<div class="topnav">
  <a class="active" href="{% url 'home'%}">Hacker News</a>
  <a href="{% url 'new_home'%}">New</a>
  <a href="{% url 'past_home'%}">Past</a>
  <a href="{% url 'submit'%}">Submit</a>

  {% if request.user.is_authenticated %}
    <div class="topnav-right">
      <a href="{% url 'signout' %}">Sign Out </a>
    </div>
  {% else %}
    <div class="topnav-right">
      <a href="{% url 'signin' %}">Sign In </a>
    </div>
  {% endif %}
</div>

<div class="w3-panel w3-light-grey w3-leftbar w3-border-grey">
<p><strong>User: </strong>{{user.username}}</p>
<p><strong>Created: </strong>{{user.date_joined}}</p>
</div>

<a href="{% url 'user_posts' user.username %}">Submissions</a>

{% endblock %}

user_post.html:

{% extends 'base.html' %}
{% block content %}

<div class="topnav">
  <a class="active" href="{% url 'home'%}">Hacker News</a>
  <a href="{% url 'new_home'%}">New</a>
  <a href="{% url 'past_home'%}">Past</a>
  <a href="{% url 'submit'%}">Submit</a>

  {% if request.user.is_authenticated %}
    <div class="topnav-right">
      <a href="{% url 'signout' %}">Sign Out </a>
    </div>
  {% else %}
    <div class="topnav-right">
      <a href="{% url 'signin' %}">Sign In </a>
    </div>
  {% endif %}
</div>

<ol>
{%for post in posts%}
  <div class="w3-panel w3-light-grey w3-leftbar w3-border-grey">
  <li><p><a href = "{{post.url}}">{{post.title}}</a></p>
  <p>{{post.votes}} | Created {{post.created_on}}| <a href = "{% url 'user_info' post.creator.username %}">{{post.creator.username}}</a> | <a href="{% url 'post' post.id %}"> {{post.comments}} Comments</a></p></li>
</div>
{% endfor %}
</ol>
{% endblock %}

4. Submit and the Edit View

Okay, now, let’s code the Submit View and the Edit View. If the user is logged in, the Submit page should display a submit Form.

The Edit Page would also do the same job, but it will update an existing post instead of creating a new one.

So the two Function Views will be:

from datetime import datetime

def SubmitPostView(request):
    if request.user.is_authenticated:
        form = PostForm()

        if request.method == "POST":
            form = PostForm(request.POST)

            if form.is_valid():
                title = form.cleaned_data['title']
                url = form.cleaned_data['url']
                description = form.cleaned_data['description']
                creator = request.user
                created_on = datetime.now()

                post = Post(title=title, url=url, description=description, creator = creator, created_on=created_on)
                post.save()
                return redirect('/')
        return render(request,'submit.html',{'form':form})
    return redirect('/signin')


def EditPostView(request,id):
    post = get_object_or_404(Post,id=id)
    if request.method =='POST':
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()
            return redirect('/')
    
    form = PostForm(instance =post)
    return render(request,'submit.html',{'form':form})

In the SubmitPostView, we are creating an entire new Post object, while in the EditPostView, we are just updating the existing one.

The URL enpoints for the two views are:

path('submit',SubmitPostView, name='submit'),
path('edit/<int:id>',EditListView, name='edit')

Also add the PostForm in the forms.py file:

from django import forms
from .models import Comment,Post


class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title','url','description')

Also, the template for them is the same since both are showing the same form

Therefore, submit.html:

{% extends 'base.html' %}
{% block content %}


<div class="topnav">
    <a class="active" href="{% url 'home'%}">Hacker News</a>
    {% if request.user.is_authenticated %}
      <div class="topnav-right">
        <a href="{% url 'signout' %}">Sign Out </a>
      </div>
    {% else %}    
      <div class="topnav-right">
        <a href="{% url 'signin' %}">Sign In</a>
      </div>
    {% endif %}
</div>


<div class="w3-panel w3-light-grey w3-leftbar w3-border-grey">
<form method ='post'>
    {% csrf_token %}
    {{form.as_p}}
    <input type="submit" value = "Submit">
</form>
</div>
{% endblock %}

5. Sign-up, Sign-in and the Sign-out View

Here we will use the django.contrib.auth library to authenticate, login, and log-out the users.

Also, we will make use of the Built-in Django User Model, AuthenticationForm, and the UserCreationForm.

So the Views will be:

from django.contrib.auth import authenticate,login,logout
from django.contrib.auth.forms import AuthenticationForm,UserCreationForm

def signup(request):
    if request.user.is_authenticated:
        return redirect('/')
    
    if request.method == 'POST':
        form = UserCreationForm(request.POST)

        if form.is_valid():
            form.save()
            username = form.cleaned_data['username']
            password = form.cleaned_data['password1']
            user = authenticate(username = username,password = password)
            login(request, user)
            return redirect('/')
        
        else:
            return render(request,'auth_signup.html',{'form':form})
    
    else:
        form = UserCreationForm()
        return render(request,'auth_signup.html',{'form':form})


def signin(request):
    if request.user.is_authenticated:
        return redirect('/')
    
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(request, username =username, password = password)

        if user is not None:
            login(request,user)
            return redirect('/')
        else:
            form = AuthenticationForm()
            return render(request,'auth_signin.html',{'form':form})
    
    else:
        form = AuthenticationForm()
        return render(request, 'auth_signin.html', {'form':form})


def signout(request):
    logout(request)
    return redirect('/')

The URL endpoints for the views:

path('signin',signin, name='signin'),
path('signup',signup, name='signup'),
path('signout',signout, name='signout'),

Both auth_signup.html and auth_signin.html will display forms to take user credentials.

Hence, auth_signup.html will be:

{% extends 'base.html' %}
{% block content %}

<div class="topnav">
  <a class="active" href="{% url 'home'%}">Hacker News</a>
  <a href="{% url 'new_home'%}">New</a>
  <a href="{% url 'past_home'%}">Past</a>
  <a href="{% url 'submit'%}">Submit</a>
</div>

<form method ='post'>
    {% csrf_token %}
    {{form.as_p}}
    <input type="submit" value = "Submit">
</form>
<br>

<h3>Already Have an Account??</h3>
<a href = "{% url 'signin' %}">Sign In Here</a>

{% endblock %}

and auth_signin.html

{% extends 'base.html' %}
{% block content %}

<div class="topnav">
  <a class="active" href="{% url 'home'%}">Hacker News</a>
  <a href="{% url 'new_home'%}">New</a>
  <a href="{% url 'past_home'%}">Past</a>
  <a href="{% url 'submit'%}">Submit</a>
</div>

<form method ='post'>
    {% csrf_token %}
    {{form.as_p}}
    <input type="submit" value = "Submit">
</form>
<br>
<h3>Dont have an account??</h3>
<a href = "{% url 'signup' %}">SignUp Here</a>

{% endblock %}

6. Coding the Upvote and Downvote logic

Whenever a user clicks on Upvote button, the number of votes of that post should increase by one and vice-versa for Downvote.

Also, note that a user cannot upvote/downvote more than once on a particular Post. So let us now code the view for both Upvote and Downvote

def UpVoteView(request,id):
    if request.user.is_authenticated:
        post = Post.objects.get(id=id)
        votes = Vote.objects.filter(post = post)
        v = votes.filter(voter = request.user)
        if len(v) == 0:
            upvote = Vote(voter=request.user,post=post)
            upvote.save()
            return redirect('/')
    return redirect('/signin')


def DownVoteView(request,id):
    if request.user.is_authenticated:
        post = Post.objects.get(id=id)
        votes = Vote.objects.filter(post = post)
        v = votes.filter(voter = request.user)
        if len(v) != 0:
            v.delete()
            return redirect('/')
    return redirect('/signin')    

Here the logic is simple:

  • UpVoteView: If for a particular post, the number of votes by a specific user is equal to zero, then create and save a new upvote of that user in the Vote Model.
  • DownVoteView: If for a particular post, the number of votes by a specific user is not equal to, i.e., more than zero, then delete the upvote of that user from Vote Model

The URL endpoints for the two:

path('vote/<int:id>',UpVoteView,name='vote'),
path('downvote/<int:id>',DownVoteView,name='dvote'),

Nice !!

7. Coding the Comment Page View

Now comes the most exciting part of the Project. The Comment View should display the comment form. Also, it should show the comments and corresponding replies in the correct thread-wise order.

That is, the comments should be shown in the below order only: C1 then C1-Child then C1-Child’s Child, then C2, C2-child, and so on.

Thread Sequence
Thread Sequence

To do that, we will use a recursive function with the identifier and the parent instance as arguments. Hence for a particular post = post.

The recursive function looks like this:

def func(i,parent):
    children = Comment.objects.filter(post =post).filter(identifier =i).filter(parent=parent)
    for child in children:
        gchildren = Comment.objects.filter(post =post).filter(identifier = i+1).filter(parent=child)
        
        if len(gchildren)==0:
            comments.append(child)
        else:
            func(i+1,child)
            comments.append(child)

We first, get all the children’s comments for a particular parent comment. Then we find how many children(ie gchildren) does each child instance have.

If the child doesn’t have any grandchild (gchild), ie. it is the bottom-most reply for that parent comment. Hence we save the child into an empty list.

If the child has “gchildren,” then we use the function again with the child as the parent argument. We do this till we reach to the bottom of the thread. After we get there, we add that comment instance to the list.

Hence each thread will be added to the list in reverse order with the bottom-most thread comment saved first and the topmost saved last.

But, we need to display the Comment Threads in the correct order with comment (identifier =0) on top and the subsequent replies below it. So before displaying them, we use the reversed(list) attribute of Python lists.

Therefore, the full CommentView will be:

def CommentListView(request,id):
    form = CommentForm()
    post = Post.objects.get(id =id)
    post.count_votes()
    post.count_comments()

    comments = []    
    def func(i,parent):
        children = Comment.objects.filter(post =post).filter(identifier =i).filter(parent=parent)
        for child in children:
            gchildren = Comment.objects.filter(post =post).filter(identifier = i+1).filter(parent=child)
            if len(gchildren)==0:
                comments.append(child)
            else:
                func(i+1,child)
                comments.append(child)
    func(0,None)

    if request.method == "POST":
        if request.user.is_authenticated:
            form = CommentForm(request.POST)
            if form.is_valid():
                content = form.cleaned_data['content']
                comment = Comment(creator = request.user,post = post,content = content,identifier =0)
                comment.save()
                return redirect(f'/post/{id}')
        return redirect('/signin')

    context ={
        'form': form,
        'post': post,
        'comments': list(reversed(comments)),
    }
    return render(request,'commentpost.html', context)

We call func(0,None) since we want the full comment threads.

The URL endpoint for the view:

path('post/<int:id>',CommentListView, name='post')

Also we need a comment form to submit the comment. Hence in forms.py, add the CommentForm:

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('content',)

And the commentpost.html should show the form and the thread comments.

So commentpost.html:

{% extends 'base.html' %}
{% block content %}

<div class="topnav">
  <a class="active" href="{% url 'home'%}">Hacker News</a>
  <a href="{% url 'new_home'%}">New</a>
  <a href="{% url 'past_home'%}">Past</a>
  <a href="{% url 'submit'%}">Submit</a>

  {% if request.user.is_authenticated %}
    <div class="topnav-right">
      <a href="{% url 'signout' %}">Sign Out </a>
    </div>
  {% else %}
    <div class="topnav-right">
      <a href="{% url 'signin' %}">Sign In </a>
    </div>
  {% endif %}
</div>

<div class="w3-panel w3-light-grey w3-leftbar w3-border-grey">
<p><a href = "{{post.url}}"><strong>Title: {{post.title}}</strong></a></p>
{% if post.creator == request.user%}
<p>{{post.votes}} votes | Created {{post.created_on}}| <a href = "{% url 'user_info' post.creator.username %}">{{post.creator.username}}</a> | {{post.comments}} Comments | <a href="{% url 'edit' post.id %}"> Edit</a></p>
{%else %}
<p>{{post.votes}} votes | Created {{post.created_on}}| <a href = "{% url 'user_info' post.creator.username %}">{{post.creator.username}}</a> | {{post.comments}} Comments</p>
{% endif %}
<p><strong>Description: </strong>{{post.description}}</p>



<form method ='post'>
    {% csrf_token %}
    {{form.as_p}}
    <input type="submit" value = "Submit">
</form>
<br>
</div>

{% for comment in comments %}
{% if comment.identifier %}
<div class="w3-panel w3-orange w3-leftbar w3-border-red">
<p class="ex1" style="font-family:helvetica;" style="color:black"><a href = "{% url 'user_info' comment.creator.username %}">Comment by: {{comment.creator.username}}</a> | Thread Level: {{comment.identifier}}</p>
<p class="ex1" style="font-family:helvetica;" style="color:black"><strong>{{comment.content}}</strong></p>
<p class="ex1" style="font-family:helvetica;" style="color:black"><a href = "{% url 'reply' id1=post.id id2=comment.id %}">reply</a></p>
</div>
{% else %}
<div class="w3-panel w3-red w3-leftbar w3-border-orange">
<p style="font-family:helvetica;" style="color:black"><a href = "{% url 'user_info' comment.creator.username %}">Comment by: {{comment.creator.username}}</a> | Thread Level: {{comment.identifier}}</p>
<p style="font-family:helvetica;" style="color:black"><strong>{{comment.content}}</strong></p>
<p style="font-family:helvetica;" style="color:black"><a href = "{% url 'reply' id1=post.id id2=comment.id %}">reply</a></p>
</div>
{% endif %}
{% endfor %}

8. Coding the Reply-Comment View

Now, when we click on the reply button below a comment, we should get a Form to submit our reply.

Hence the CommentReplyView will be:

def CommentReplyView(request,id1,id2):
    form = CommentForm()
    comment = Comment.objects.get(id = id2)
    post = Post.objects.get(id=id1)

    if request.method == "POST":
        if request.user.is_authenticated:
            form = CommentForm(request.POST)
            
            if form.is_valid():
                reply_comment_content = form.cleaned_data['content']
                identifier = int(comment.identifier + 1)

                reply_comment = Comment(creator = request.user, post = post, content = reply_comment_content, parent=comment, identifier= identifier)
                reply_comment.save()

                return redirect(f'/post/{id1}')
        return redirect('/signin')
    
    context ={
        'form': form,
        'post': post,
        'comment': comment,
    }
    return render(request,'reply_post.html', context)

The Reply Comments will have a parent instance, unlike regular Post comments, which have parent instance = None.

The URL endpoint for the view:

path('post/<int:id1>/comment/<int:id2>',CommentReplyView,name='reply'),

The reply_post.html should display the Parent Comment Instance and the Reply Form.

Hence the template reply_post.html will be:

{% extends 'base.html' %}
{% block content %}

<div class="topnav">
  <a class="active" href="{% url 'home'%}">Hacker News</a>
  <a href="{% url 'new_home'%}">New</a>
  <a href="{% url 'past_home'%}">Past</a>
  <a href="{% url 'submit'%}">Submit</a>

  {% if request.user.is_authenticated %}
    <div class="topnav-right">
      <a href="{% url 'signout' %}">Sign Out </a>
    </div>
  {% else %}
    <div class="topnav-right">
      <a href="{% url 'signin' %}">Sign In </a>
    </div>
  {% endif %}
</div>

<div class="w3-panel w3-light-grey w3-leftbar w3-border-grey">
<p> <h5><a href = "{% url 'user_info' comment.creator.username %}">{{comment.creator.username}}</a> | On : <a href = "{% url 'post' post.id %}">{{post.title}}</a></h5></p>
<p>{{comment.content}}</p>

<form method ='post'>
    {% csrf_token %}
    {{form.as_p}}
    <input type="submit" value = "Submit">
</form>
</div>
{% endblock %}

Great !! That’s it guys !!

Final Code for the Django Project

The whole project can be found in my Github profile. Feel free to clone the repository in your systems and play around with the code. I’m also posting the complete code for each of the files below for your convenience.

1. Models.py

from django.db import models
from django.contrib.auth.models import User
# Create your models here.

class Post(models.Model):
    title = models.CharField("HeadLine", max_length=256, unique=True)
    creator = models.ForeignKey(User, on_delete= models.SET_NULL, null=True)
    created_on = models.DateTimeField(auto_now_add=True)
    url = models.URLField("URL", max_length=256,blank=True)
    description = models.TextField("Description", blank=True)
    votes = models.IntegerField(null=True)
    comments = models.IntegerField(null=True)    

    def __unicode__(self):
        return self.title

    def count_votes(self):
        self.votes = Vote.objects.filter(post = self).count()
    
    def count_comments(self):
        self.comments = Comment.objects.filter(post = self).count()



class Vote(models.Model):
    voter = models.ForeignKey(User, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)

    def __unicode__(self):
        return f"{self.user.username} upvoted {self.link.title}" 


class Comment(models.Model):
    creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    content = models.TextField()
    identifier = models.IntegerField()
    parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True)

    def __unicode__(self):
        return f"Comment by {self.user.username}"

2. Views.py

from django.shortcuts import render,redirect,get_object_or_404
from django.views.generic import ListView
from .models import Post,Vote,Comment
from .forms import CommentForm,PostForm

from django.contrib.auth.models import User
from django.contrib.auth import authenticate,login,logout
from django.contrib.auth.forms import AuthenticationForm,UserCreationForm

from datetime import datetime,timedelta
from django.utils import timezone

from django.contrib.auth import authenticate,login,logout
from django.contrib.auth.forms import AuthenticationForm,UserCreationForm
# Create your views here.


def PostListView(request):
    posts = Post.objects.all()
    for post in posts:
        post.count_votes()
        post.count_comments()
        
    context = {
        'posts': posts,
    }
    return render(request,'hnapp/postlist.html',context)


def NewPostListView(request):
    posts = Post.objects.all().order_by('-created_on')
    for post in posts:
        post.count_votes()
        post.count_comments()    
    context = {
        'posts': posts,
    }
    return render(request,'hnapp/postlist.html', context)


def PastPostListView(request):
    time = str((datetime.now(tz=timezone.utc) - timedelta(minutes=30)))
    posts = Post.objects.filter(created_on__lte = time)
    for post in posts:
        post.count_votes()
        post.count_comments()

    context={
        'posts': posts,
    }
    return render(request,'hnapp/postlist.html',context)


def UpVoteView(request,id):
    if request.user.is_authenticated:
        post = Post.objects.get(id=id)
        votes = Vote.objects.filter(post = post)
        v = votes.filter(voter = request.user)
        if len(v) == 0:
            upvote = Vote(voter=request.user,post=post)
            upvote.save()
            return redirect('/')
    return redirect('/signin')


def DownVoteView(request,id):
    if request.user.is_authenticated:
        post = Post.objects.get(id=id)
        votes = Vote.objects.filter(post = post)
        v = votes.filter(voter = request.user)
        if len(v) != 0:
            v.delete()
            return redirect('/')
    return redirect('/signin')    


def UserInfoView(request,username):
    user = User.objects.get(username=username)
    context = {'user':user,}
    return render(request,'hnapp/userinfo.html',context)


def UserSubmissions(request,username):
    user = User.objects.get(username=username)
    posts = Post.objects.filter(creator = user)
    print(len(posts))
    for post in posts:
        post.count_votes()
        post.count_comments()    
    return render(request,'hnapp/user_posts.html',{'posts': posts})
  

def EditListView(request,id):
    post = get_object_or_404(Post,id=id)
    if request.method =='POST':
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()
            return redirect('/')
    
    form = PostForm(instance =post)
    return render(request,'hnapp/submit.html',{'form':form})


def CommentListView(request,id):
    form = CommentForm()
    post = Post.objects.get(id =id)
    post.count_votes()
    post.count_comments()

    comments = []    
    def func(i,parent):
        children = Comment.objects.filter(post =post).filter(identifier =i).filter(parent=parent)
        for child in children:
            gchildren = Comment.objects.filter(post =post).filter(identifier = i+1).filter(parent=child)
            if len(gchildren)==0:
                comments.append(child)
            else:
                func(i+1,child)
                comments.append(child)
    func(0,None)

    if request.method == "POST":
        if request.user.is_authenticated:
            form = CommentForm(request.POST)
            if form.is_valid():
                content = form.cleaned_data['content']
                comment = Comment(creator = request.user,post = post,content = content,identifier =0)
                comment.save()
                return redirect(f'/post/{id}')
        return redirect('/signin')

    context ={
        'form': form,
        'post': post,
        'comments': list(reversed(comments)),
    }
    return render(request,'hnapp/post.html', context)


def CommentReplyView(request,id1,id2):
    form = CommentForm()
    comment = Comment.objects.get(id = id2)
    post = Post.objects.get(id=id1)

    if request.method == "POST":
        if request.user.is_authenticated:
            form = CommentForm(request.POST)
            
            if form.is_valid():
                reply_comment_content = form.cleaned_data['content']
                identifier = int(comment.identifier + 1)

                reply_comment = Comment(creator = request.user, post = post, content = reply_comment_content, parent=comment, identifier= identifier)
                reply_comment.save()

                return redirect(f'/post/{id1}')
        return redirect('/signin')
    
    context ={
        'form': form,
        'post': post,
        'comment': comment,
    }
    return render(request,'hnapp/reply_post.html', context)


def SubmitPostView(request):
    if request.user.is_authenticated:
        form = PostForm()

        if request.method == "POST":
            form = PostForm(request.POST)

            if form.is_valid():
                title = form.cleaned_data['title']
                url = form.cleaned_data['url']
                description = form.cleaned_data['description']
                creator = request.user
                created_on = datetime.now()

                post = Post(title=title, url=url, description=description, creator = creator, created_on=created_on)

                post.save()
                return redirect('/')
        return render(request,'hnapp/submit.html',{'form':form})
    return redirect('/signin')


def signup(request):

    if request.user.is_authenticated:
        return redirect('/')
    
    if request.method == 'POST':
        form = UserCreationForm(request.POST)

        if form.is_valid():
            form.save()
            username = form.cleaned_data['username']
            password = form.cleaned_data['password1']
            user = authenticate(username = username,password = password)
            login(request, user)
            return redirect('/')
        
        else:
            return render(request,'hnapp/auth_signup.html',{'form':form})
    
    else:
        form = UserCreationForm()
        return render(request,'hnapp/auth_signup.html',{'form':form})


def signin(request):
    if request.user.is_authenticated:
        return redirect('/')
    
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(request, username =username, password = password)

        if user is not None:
            login(request,user)
            return redirect('/')
        else:
            form = AuthenticationForm()
            return render(request,'hnapp/auth_signin.html',{'form':form})
    
    else:
        form = AuthenticationForm()
        return render(request, 'hnapp/auth_signin.html', {'form':form})


def signout(request):
    logout(request)
    return redirect('/')

3. Urls.py

from django.contrib import admin
from django.urls import path
from .views import *

urlpatterns = [
    path('',PostListView, name='home'),
    path('new',NewPostListView, name='new_home'),
    path('past',PastPostListView, name='past_home'),
    path('user/<username>', UserInfoView, name='user_info'),
    path('posts/<username>',UserSubmissions, name='user_posts'),
    path('post/<int:id>',CommentListView, name='post'),
    path('submit',SubmitPostView, name='submit'),
    path('signin',signin, name='signin'),
    path('signup',signup, name='signup'),
    path('signout',signout, name='signout'),
    path('vote/<int:id>',UpVoteView,name='vote'),
    path('downvote/<int:id>',DownVoteView,name='dvote'),
    path('edit/<int:id>',EditListView, name='edit'),
    path('post/<int:id1>/comment/<int:id2>',CommentReplyView,name='reply'),
]

4. Forms.py

from django import forms
from .models import Comment,Post

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('content',)

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title','url','description')

6. Admin.py

from django.contrib import admin
from .models import *

# Register your models here.
admin.site.register(Post)
admin.site.register(Vote)
admin.site.register(Comment)
#admin.site.register(UserInfo)

Implementation of the Code

That’s it with the coding part!! Now run the server

python manage.py runserver

and go to the home page: “www.localhost.com

Home Page
Home Page

No Posts are there, so click on Sign-In:

Signin
Signin

Click on SignUp Here and register your account

Signup
Signup

Once you are done registering, go to submit and add a few posts

Submit
Submit

I have submitted a few posts in there, so now click on the Hacker News button on the Nav Bar to reach the home page:

Home Page Posts
Home Page Posts

You can now upvote and downvote posts. Similarly click on New and Past buttons. Now click on the username below the post – Nishant in my case:

User
User

You will see user info as well as the submissions button. Okay, now go back and click on Comments; you will reach the comments page

Post
Post

Submit a Comment

Comment
Comment

There we have, our Top most comment. Now when click on Reply:

Reply To Comment 1
Reply To Comment 1

Enter a random reply and click submit.

Reply Thread
Reply Thread

See the Thread level = 1, has rearranged itself such that the parent comment is on top. This is what the recursive function is doing. Try adding some more replies and see how it arranges itself.

Great !! Our very own Django project idea coming into reality.

References

Conclusion

That’s it, guys! Your Own Hacker News Web App is ready. Do Try to implement all the logic codes on your own for better understanding!