DjangoでSNSアプリ(Hacker Newsクローン)を自作する方法を解説する

スポンサーリンク

ハローガイズ この記事では、Django Web Framework を使って Hacker News ウェブサイトのような完全なウェブアプリケーションを構築します。

スポンサーリンク

Hacker Newsについて

Hacker Newsは、投資ファンドやスタートアップインキュベータであるY-Combinatorが運営するソーシャルニュースサイトです。

主にコンピュータサイエンスとアントレプレナーシップにフォーカスしている。

本サイトは、「知的好奇心を満たすものなら何でも共有できる」プラットフォームであると自らを定義しています。

ウェブサイトはこちらからご覧ください – Hacker News

私たちは、このウェブサイトの主な機能をすべて盛り込んだウェブアプリケーションを作ります。

ウェブサイトの面白い機能

では、Django を使って作成する Web サイトの興味深い機能を見てみましょう。

1. トップナビゲーションバー

django-admin startproject HackerNews
  • Hacker News ボタンをクリックすると、トップページに戻ります。
  • Newボタンは、最新の投稿を全て表示します。
  • Pastボタンは、30分前などの投稿を表示します。
  • 同様に、ask、show、jobsがありますが、これはあまり重要ではありません。
  • それから、投稿オプションとログアウト/ログインオプションがあります。

これらをすべてアプリでコーディングします。

2. 個別だけでなく、投稿の一覧も表示

そして、メインページに表示される投稿のリストを用意しました。

django-admin startapp hnapp
  • 各ポストには、そのポストを投票するためのUpvoteオプションがあります。
  • 各ポストには、総投票数、総コメント数が表示されます。
  • 作成者のユーザーネームが表示されます。
  • 投稿時間が表示される

また、コメントをクリックすると、コメントページにリダイレクトされます。

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

ここでは、その投稿にコメントを投稿したり、他の人に返信したりすることができます

ここでも興味深いのは、スレッドコメントを形成することです。

つまり、あるコメントに返信するとき、私たちの返信はそのコメントのすぐ下に表示されます。

この機能は簡単ではありませんが、次のセクションで説明します。

3. ユーザー認証

もう一つの重要な機能として、ユーザー認証があります。

Webサイトでは、アカウントを持っている人だけが投稿やコメント、返信ができます。

<!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>

サインアップビュー

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

この2つのビューを再びコードに含めます!

4. 投稿ビュー

このサイトには投稿ビューがあります。

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()

ここでは、記事のタイトル、URL、説明文を投稿することができます

というわけで、これでおしまいです。

これは、私たちがしなければならないことです。

Django ウェブアプリケーションで Hacker News クローンをコーディングする

F

1. 必要なモデルのコーディング

Webサイトには、以下のものが必要です。

  • Postモデル。投稿情報を格納する
  • Vote モデル。各投稿の Upvote を格納する
  • Comment モデル。各ポストのコメントを格納します。

そして、ユーザーアカウント情報を格納するための、あらかじめ用意されたUserモデルです。

そこで、models.pyに以下のモデルを追加します。

Post モデル。

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}"

ここでは、各投稿の総投票数と総コメント数をカウントするための関数を2つ用意しています。

なお、作成者がアカウントを削除しても投稿は削除されないので、on_deleteをmodels.SET_NULLに設定します。

投票モデル

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}"

このモデルは、どのユーザがどの投稿にupvotedしたかという情報を格納します。

そして、最後のComment Modelです。

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

各コメントは、作成者、作成者がコメントした投稿、およびコメントの内容そのものを持ちます。

さて、各返信コメントは、親コメント、すなわち、返信が行われたコメントも持つことになります。

したがって、コメントモデル自体への外部キーである親フィールドが必要です。

また、異なるレベルの返信コメントを識別するために、もうひとつのフィールド、識別子フィールドが必要です。

これを理解するために、次の画像を考えてみましょう。

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)

したがって

  • 投稿されたコメントは、最上位のコメントであるため、identifier = 0、parent = None となります。
  • 第一階層の返信コメントは、識別子=1、親コメントは返信先のコメント(識別子=0)となります。
  • 同様に、2階層目の返信コメントは、識別子=2、親コメントが識別子=1となります。

これらの2つのフィールドを使って、どのようにスレッド形式でコメントを表示するかは、後ほど説明します。

admin.py の admin.site.register(model_name) の行を使用して、3つのモデルを登録します。

また、コードを使用してマイグレーションを実行することを忘れないでください。

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

2. ビューとそれに対応するテンプレートのコード化

モデルを配置したので、次はビューをコーディングしましょう。

さて、完全なウェブサイトには、次のようなビューが必要です。

    1. ホームページビュー。投稿のリストを表示する
  1. 新しい投稿の表示。最新の投稿を表示します。
  2. 過去の投稿を表示します。30分以上前の投稿を表示します。
  3. シングルポストビュー。コメントフォームと既存のコメントを表示する
  4. Reply-Comment View。既存のコメントに返信するためのフォームを表示する
  5. ユーザー情報ビュー。ユーザーに関する情報を表示します。
  6. ユーザー投稿ビュー。特定のユーザーの投稿を表示する
  7. 投稿ビュー。投稿フォームを表示する
  8. Edit ビュー。投稿されたフォームを編集する
  9. Sign in ビュー。サインインページを表示します。
  10. サインアップ画面です。サインアップページを表示する
  11. サインアウト画面。ユーザーをログアウトさせる

これとは別に、投稿のUpVotingとDownVotingを処理するために、もう2つのViewが必要です。

すごい数のViewがありますね。

では、さっそく始めてみましょう。

1. ホームページの表示

というわけで、Views.py に PostListView (Home page) 機能のビューを追加します。

{% 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 %}

各ポストについて、総投票数と総コメント数をカウントしてからウェブページに表示します。

urls.pyのurlエンドポイントです。

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)

Django App フォルダ自体の templates フォルダに postlist.html Template を追加します。

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)

ここでは、ユーザがログインしている場合は NavBar にサインアウトを、ログインしていない場合はサインインを表示するようにします。

各 Post には、Title、Creator、Creation Date and Time、Total Votes、Comments が表示されます。

また、投稿の作成者がユーザー自身である場合は、編集オプションも表示されます。

2. 新着記事と過去の記事表示

新着記事ビューでは、最新の投稿が最初に表示されます。

そのためのコードは次のようになります。

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

同様に、30分以上前に作成された投稿を表示するには、PythonのDateTimeライブラリを使用します。

したがって、コードは次のようになります。

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})

ここで、”less than or equal “は、”week “を意味します。

つまり、30分前の時間よりも短い時間の投稿をフィルタリングしています。

ビューのurlエンドポイント。

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

この2つのテンプレートは、トップページビューと同じになります。

3. ユーザー情報およびユーザー投稿の表示

クライアントが投稿の作成者名をクリックすると、ユーザ情報ページに到達するようにします。

ユーザー情報ページには、ユーザー名、作成日、そしてユーザーの投稿を表示するページへのリンクが表示される必要があります。

では、ユーザー情報とユーザー投稿の両方のビューをここでコーディングしましょう。

{% 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 %}

UserSubmissionsビューでは、投稿を表示する前に、forループを使って投票数とコメント数の合計を計算しています。

ビューのurlエンポイントは以下の通りです。

{% 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 %}

また、対応するテンプレートは以下の通りです。

user_info.html となります。

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})

user_post.html。

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

4. SubmitとEdit View

では、Submit ビューと Edit ビューをコーディングしてみましょう。

ユーザーがログインしている場合、Submit ページは投稿フォームを表示するはずです。

編集ページも同じ役割を果たしますが、新しい投稿を作成するのではなく、既存の投稿を更新することになります。

つまり、2つの機能ビューが存在することになります。

from django import forms
from .models import Comment,Post
 
 
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title','url','description')

SubmitPostViewでは、まったく新しいPostオブジェクトを作成し、EditPostViewでは、既存のものを更新しているだけです。

この2つのビューのURLエンポイントは次のとおりです。

{% 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 %}

また、forms.py ファイルに PostForm を追加します。

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('/')

また、どちらも同じフォームを表示しているので、テンプレートも同じになります。

したがって、submit.htmlを作成します。

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

5. サインアップ、サインイン、そしてサインアウトビュー

ここでは、 django.contrib.auth ライブラリを使用して、ユーザの認証、ログイン、ログアウトを行います。

また、組み込みの Django User Model、 AuthenticationForm、そして UserCreationForm を利用します。

そのため、Views は以下のようになります。

{% 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 %}

ビューの URL エンドポイント。

{% 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 %}

auth_signup.html と auth_signin.html はどちらもユーザの認証情報を取得するためのフォームを表示します。

したがって、auth_signup.htmlは以下のようになります。

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')   

となり、auth_sequin.htmlは

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

6. UpvoteとDownvoteのロジックをコーディングする

ユーザーが Upvote ボタンをクリックするたびに、その投稿の投票数が 1 つ増え、Downvote の場合はその逆になります。

また、一人のユーザーが特定の投稿に複数回アップヴォート/ダウンヴォートすることはできないことに注意してください。

それでは、UpvoteとDownvoteの両方を表示するコードを書いてみましょう。

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)

ここでは、ロジックはシンプルです。

  • UpVoteView: * UpVoteView: 特定の投稿に対して、特定のユーザーの投票数が0である場合、そのユーザーの新しいアップボートを作成してVote Modelに保存します。
  • DownVoteView: ある投稿に対して、特定のユーザの投票数がゼロでない場合、つまりゼロより多い場合、そのユーザのアップボートをVote Modelから削除します。

の2つのURLエンドポイント。

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)

ナイス!

7. コメントページビューのコーディング

さて、このプロジェクトの最もエキサイティングな部分がやってきました。

コメントビューは、コメントフォームを表示する必要があります。

また、コメントとそれに対応する返信をスレッドごとに正しい順序で表示する必要があります。

つまり、コメントは以下の順序で表示されなければなりません。

C1、C1-Child、C1-Childの子、C2、C2-Child、といった具合です。

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

そのために、識別子と親インスタンスを引数として再帰関数を使う。

したがって、ある特定のpost = postについてです。

再帰的関数は次のようになる。

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

まず、特定の親コメントに対するすべての子コメントを取得します。

次に、それぞれの子インスタンスがいくつの子(つまりgchildren)を持っているかを調べます。

もしその子が孫(gchild)を持っていなければ、それはその親コメントに対する最下位の返信です。

したがって、その子は空のリストに保存されます。

もしその子が「gchildren」を持っていたら、その子を親の引数として関数を再度使用します。

これをスレッドの最下部に到達するまで続けます。

そこまで到達したら、そのコメントインスタンスをリストに追加します。

したがって、各スレッドは逆順にリストに追加され、一番下のスレッドのコメントが最初に保存され、一番上のスレッドのコメントが最後に保存されることになります。

しかし、コメントスレッドを正しい順序で表示するには、コメント(識別子=0)を一番上に、それ以降の返信をその下に表示する必要があります。

そこで、それらを表示する前に、Pythonのリストのreversed(list)属性を使っています。

したがって、完全なCommentViewは次のようになります。

{% 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 %}

コメントスレッド全体を表示したいので、func(0,None)を呼び出します。

ビューの URL エンドポイントです。

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)

また、コメントを送信するためにCommentFormが必要です。

したがって、forms.pyにCommentFormを追加します。

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

そしてcommentpost.htmlには、フォームとスレッドコメントを表示します。

というわけで、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> <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 %}

8. コメント返信ビューのコード化

さて、コメントの下にある返信ボタンをクリックすると、返信を送信するためのフォームが表示されるはずです。

したがって、CommentReplyViewは次のようになります。

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}"

通常の Post コメントが parent instance = None であるのとは異なり、Reply Comments は parent instance を持ちます。

ビューのURLエンドポイントです。

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('/')

reply_post.htmlは、親コメントのインスタンスと返信フォームを表示します。

したがって、reply_post.htmlのテンプレートは次のようになります。

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'),
]

素晴らしい! 以上です。

Django プロジェクトの最終コード

プロジェクト全体は、私の Github プロファイルにあります。

あなたのシステムでレポジトリをクローンして、自由にコードを弄ってください。

また、便宜上、各ファイルの完全なコードを以下に掲載します。

1. モデル.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')

2. ビュー.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)

3. Urls.py

python manage.py runserver

4. Forms.py

NavBar
HackerNews NavBar

6. Admin.py

List Of Posts
List Of Posts

コードの実装

コーディングの部分はこれで終わりです! では、サーバーを動かしてみましょう

Post
Post

を実行して、ホームページにアクセスします。

Signin
Signin

投稿がないので、「サインイン」をクリックします。

Sign Up
Sign Up

SignUp Hereをクリックし、アカウントを登録します。

Submit
Submit

登録が完了したら、submitから投稿を追加してください。

Templates
Templates

投稿が完了したら、ナビバーのHacker Newsボタンをクリックし、トップページに移動してください。

Base
Base

これで、投稿にアップヴォートとダウンヴォートをすることができます

同様に、新規投稿と過去投稿のボタンをクリックします。

次に、投稿の下にあるユーザー名をクリックします – 私の場合は Nishant です。

Comment Threads
Comment Threads

すると、ユーザー情報と投稿ボタンが表示されます。

次に、「コメント」をクリックすると、コメントページが表示されます。

Thread Sequence
Thread Sequence

コメントを投稿する

Home Page
Home Page

ここに、私たちのトップコメントがあります。

Signin
Signin

ランダムな返信を入力し、送信ボタンをクリックします。

Signup
Signup

スレッドレベル = 1 で、親コメントが一番上になるように再配置されているのがわかります。

これが再帰関数が行っていることです。

さらに返信を追加して、どのように配置されるかを確認してみてください。

素晴らしい! 私たち独自のWebアプリケーションプロジェクト

参考文献

  • Django Models: https://www.askpython.com/django/django-models
  • Django ビュー: https://www.askpython.com/django/django-views
  • Django テンプレート: https://www.askpython.com/django/django-templates
  • Django モデルフォーム: https://www.askpython.com/django/django-model-forms

まとめ

以上、皆さん! あなただけの Hacker News Web App が完成しました。

よりよく理解するために、すべてのロジックコードを自分で実装してみてください。

それでは、Happy Coding!

タイトルとURLをコピーしました