Python(Django)でSNSアプリケーションの開発Part3~タイムライン実装~

[前回の記事](https://qiita.com/Taku_nakahara/items/e6268ae9ff5b51b042d9)まででユーザー認証機能の実装が完了しました。今回はタイムライン機能について実装しましょう。
改めて必要な機能について整理しましょう。

<h5>必要な機能について</h5>

①投稿一覧(タイムライン)
②投稿詳細
③投稿機能
④投稿編集機能
⑤返信機能
⑥ユーザーごとの投稿

最終的なサイトのデザイン等については[こちらの記事](https://qiita.com/Taku_nakahara/items/50ee5d98a7e86fcd8475)を参照ください。

<h2>タイムライン機能の実装</h2>
ここまでの時点でディレクトリ構成は以下のようになっていると思います。

```Python:ディレクトリ構成
app_sns/
        ├プロジェクト
        ├ manage.py
        └ accounts(app_name)#前回の記事で実装
        └ timelines(app_name)/
            ├ __init__.py
            ├ apps.py
            ├ models.py
            ├ tests.py
            ├ views.py
            └ urls.py#なければ追記
        └ templates/
        └ static/
            └ css/style.css
```
ここではまず以下のことをしてください。
①timelinesにforms.pyとcontext.pyを追加する。
②templatesに以下のhtmlファイルを追加する。
・top.html
・index.html
・detail.html
・form.html
・delete.html
・update.html
・comment.html
・userpost.html

③staticフォルダにjsというフォルダを作成し,その中にindex.jsを作成する。
<h5>コード全文</h5>

```Python:urls.py
from django.urls import path
from . import views

app_name = 'timelines'

urlpatterns = [
    path('top/', views.TopView.as_view(), name = 'top'),
    path('', views.IndexView.as_view(), name = 'index'),
    path('<int:pk>/', views.DetailView.as_view(), name = 'detail'),
    path('create/', views.PostCreateView.as_view(), name = 'create'),
    path('delete/<int:pk>/', views.PostDeleteView.as_view(), name = 'delete'),
    path('update/<int:pk>/', views.PostUpdateView.as_view(), name = 'update'),
    path('comment/<int:pk>/', views.CommentCreateView.as_view(), name = 'comment'),
    path('userpost/<int:user>/', views.UserPostView.as_view(), name = 'userpost'),
]
```

```Python:models.py
from django.db import models
from accounts.models import User
# Create your models here.
class Post(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE ,verbose_name = 'ユーザー')
    text = models.TextField(verbose_name = '本文')
    created_at = models.DateTimeField(auto_now_add=True, verbose_name = '投稿日時')

    def __str__(self):
        return self.text

class Comment(models.Model):
    text = models.TextField(verbose_name = 'コメント')
    created_at = models.DateTimeField(auto_now_add=True, verbose_name = '返信日時')
    target = models.ForeignKey(Post, on_delete=models.CASCADE, verbose_name = '対象の投稿')
    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name = 'ユーザー')

    def __str__(self):
        return self.text
```

```Python:forms.py
from django import forms
from .models import Post, Comment

class PostCreateForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('text',)


class CommentCreateForm(forms.ModelForm):
    class Meta:
        model = Comment 
        fields = ('text',)
```
@method_decorator(login_required, name = 'dispatch')がついているビューはログイン状態のときのみ表示・機能するビューです。
新規作成や更新などの処理では、forms.pyで作成したModelFormを利用しています。
models.pyのPostテーブル内のtextのみが自由にDB登録でき、それ以外は自動で登録されます。(ユーザーはログインしているユーザー)
get_querysetやget_context_dataの考え方については詳細は別の記事(後日公開予定)で解説しますが、基本的にはDBから特定の情報を取得し、テンプレートにお渡し表示するという流れです。

form_validは送信時に使う関数で入力されているか判定するために利用します。
例えば新規投稿時にtextが空欄だったらエラーをはいて送信処理を中断します。
```Python:views.py
from django.shortcuts import render, get_object_or_404, redirect
from .models import Post, Comment
from .forms import PostCreateForm, CommentCreateForm
from django.urls import reverse_lazy
from django.views.generic import TemplateView, ListView, DetailView, CreateView, DeleteView, UpdateView
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.contrib import messages
from accounts.models import User
# Create your views here.
class TopView(TemplateView):
    template_name = 'top.html'

@method_decorator(login_required, name = 'dispatch')
class IndexView(ListView):
    template_name = 'index.html'
    context_object_name = 'posts'
    
    def get_queryset(self):
        queryset = Post.objects.all().order_by('-created_at')
        return queryset
    

@method_decorator(login_required, name = 'dispatch')
class DetailView(DetailView):
    model = Post
    template_name = 'detail.html'

@method_decorator(login_required, name = 'dispatch')
class PostCreateView(CreateView):
    model = Post
    form_class = PostCreateForm
    template_name = 'form.html'
    success_url = reverse_lazy('timelines:index')

    def form_valid(self, form):
        pd = form.save(commit=False)
        pd.user = self.request.user
        pd.save()
        return super().form_valid(form)

@method_decorator(login_required, name = 'dispatch')
class PostDeleteView(DeleteView):
    model = Post
    template_name = 'delete.html'
    success_url = reverse_lazy('timelines:index')

    def delete(self,request, *args, **kwargs):
        return super().delete(request, *args, **kwargs)
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        print(context)
        return context

@method_decorator(login_required, name = 'dispatch')
class PostUpdateView(UpdateView):
    model = Post 
    template_name = 'update.html'
    form_class = PostCreateForm
    success_url = reverse_lazy('timelines:index')

@method_decorator(login_required, name = 'dispatch')
class CommentCreateView(CreateView):
    model = Comment
    template_name = 'comment.html'
    form_class = CommentCreateForm
    success_url = reverse_lazy('timelines:detail')

    def form_valid(self, form):
        post_pk = self.kwargs['pk']
        post = get_object_or_404(Post, pk=post_pk)
        comment = form.save(commit=False)
        comment.target = post  
        comment.user = self.request.user
        comment.save()
        return redirect('timelines:detail', pk=post.pk)

class UserPostView(ListView):
    template_name = 'userpost.html'

    def get_queryset(self):
        user_id = self.kwargs['user']
        queryset = Post.objects.filter(user = user_id)
        return queryset
```

```Python:context.py
from timelines.models import Post

def common(request):
    context = {
        'user_list':Post.objects.all(), 
    }
    return context
```


<h5>テンプレート</h5>

```HTML:top.html
{% extends 'base.html'%}
{% load static %}

{% block contents %}

<div class = 'top-page'>
    <div class = 'top-container'>
        <h2>SNSAPP~no-title~へようこそ</h2>

        <h5>日常の緩い出来事やポジティブな出来事を発信しよう！！</h5>
        
        {% if user.is_authenticated %}
        <div class = 'topbtn'>
            <button class = 'btn-auth' onClick = "location.href = '{% url 'timelines:index' %}'">タイムラインを見よう！</a>
            <button class = 'btn-auth' onClick = "location.href = '{% url 'timelines:create' %}'">コメントを投稿しよう！</a>
        </div>
        {% else %}
        <div class = 'tab'>
            <ul class = 'tab_menu'>
                <li class = 'tab_menu-item is-active' data-tab = '01'>このサイトについて</li>
                <li class = 'tab_menu-item' data-tab = '02'>Get Start</li>
            </ul>
            <div class = 'tab_panel'>
                <div class = 'tab_panel-box tab_panel-box001 is-show' data-tab = '01'>
                    <h3>Concept</h3>
                    <p>SNSです。。。</p>
                </div>
                <div class = 'tab_panel-box tab_panel-box002' data-tab = '02'>
                    <p>アカウントをお持ちの方はこちら</p>
                    <button class = 'btn-auth' onClick = "location.href = '{% url 'accounts:login' %}'">ログインして投稿しよう！</button>
                    <br></br>
                    <p>アカウントを持っていない方はこちら</p>
                    <button class = 'btn-auth' onClick = "location.href = '{% url 'accounts:signup' %}'">アカウントを作成して投稿しよう！</button>
                </div>
            </div>
        </div>
        {% endif %}
    </div>
</div>
<script src = '{% static 'js/index.js' %}'></script>
{% endblock %}
```

```HTML:index.html
{% extends 'base.html'%}
{% load static %}

{% block contents %}
<div class = 'timeline'>
  <h3>タイムライン</h3>
</div>

        {% for post in posts %}
        <div class="card">
          <div class="card-header">
            {{ post.user }}<span class = 'created_at'>{{ post.created_at }}</span> 
          </div>
          <div class="card-body">
            <p class="card-text">{{ post.text|truncatechars:30 }}</p>
            <a href='{% url 'timelines:detail' post.pk %}' style="text-decoration: none;"  class="btn-auth">投稿詳細</a>
            <a href = '{% url 'timelines:userpost' post.user.id %}' style="text-decoration: none;" class = 'btn_auth'>
                  {{ post.user.username }}の投稿一覧へ</a>
          </div>
        </div>

        {% endfor %}
{% endblock %}
```

```HTML:detail.html
{% extends 'base.html'%}
{% load static %}

{% block contents %}
<div class="card">
    <div class="card-header">
      {{ post.user }}<span class = 'created_at'>{{ post.created_at }}</span> 
    </div>
    <div class="card-body">
      <p class="card-text">{{ post.text }}</p>
        <p>
        {% if post.user == user %}
            <a href = '{% url 'timelines:delete' post.pk %}'>削除する</a>
            <a href = '{% url 'timelines:update' post.pk %}'>更新</a>
        {% endif %}
        </p>

        <a class = 'btn-auth' onClick = "location.href = '{% url 'timelines:comment' post.pk %}'" style="text-decoration: none;">返信する</a>
        <a href = '{% url 'timelines:userpost' post.user.id %}' style="text-decoration: none;" class = 'btn_auth'>
            {{ post.user.username }}の投稿一覧へ</a>
    </div>
</div>
<div class = 'comment'>
    <h5>コメント一覧</h5>
</div>
{% for comment in post.comment_set.all %}
<div class="card">
    <div class="card-header">
        {{ comment.user }}<span class = 'created_at'>{{ comment.created_at }}</span> 
    </div>
    <div class="card-body">
      <p class="card-text">{{ comment.text }}</p>
      <a class = 'btn-auth' onClick = "location.href = '{% url 'timelines:comment' post.pk %}'" style="text-decoration: none;">返信する</a>
      <a href = '{% url 'timelines:userpost' comment.user.id %}' style="text-decoration: none;" class = 'btn_auth'>{{ comment.user.username }}の投稿一覧へ</a>
    </div>
  </div>
{% endfor %}
{% endblock %}
```

```HTML:form.html
{% extends 'base.html'%}
{% load static %}
{% block contents %}
<div class = 'form-container'>
    <div class = "form">
    <form method = 'post' action = '{% url 'timelines:create' %}'>
        {% csrf_token %}
        <h2 style = 'text-align:center'>投稿</h2>
        <div class = 'form-area'>
            {% for field in form %}
            <div class = 'userinfo'>
                <div class = 'info'>
                <label for = 'Username'><span>必須</span>投稿してください</label>
                    <div class="mb-3">
                        <textarea type="text" name = 'text' rows = '10' id = 'id_text'autocapitalize = 'none' autocomplete = 'username' class = 'form-control' placeholder = 'テキストを入力してください。' required autofocus></textarea>
                    </div>
                </div>
            </div>
            <p>
                {% if field.help_text %}
                {{ field.help_text }}
                {% endif %}
            </p>
            <p>{{ field.errors }}</p>
            {% endfor %}
            <input type = 'submit'  onClick = 'return alertFunc()' class = 'btn_auth' value = '投稿する'>
            {% if form.is_valid %}
            <script>
                function alertFunc() {
                    alert("投稿完了！タイムラインに戻ります。");
                }
            </script>
            {% endif %}
        </div>
    </form>
</div>
</div>
{% endblock %}
```

```HTML:update.html
{% extends 'base.html' %}
{% load static %}

{% block contents %}
<div class = 'form-container'>
    <div class = "form">
    <form method = 'post'>
        {% csrf_token %}
        <h2 style = 'text-align:center'>投稿</h2>
        <div class = 'form-area'>
            <div class = 'userinfo'>
                <div class = 'info'>
                <label for = 'Username'><span>必須</span>投稿してください</label>
                    <div class="mb-3">
                        {{ form.text }}
                    </div>
                </div>
            </div>

            <input type = 'submit' onClick = 'return alertFunc()' class = 'btn_auth' value = '投稿する'>
            <script>
                function alertFunc() {
                    if(document.form.input01.value == ""){
                        return false;
                    } else {
                    alert("投稿完了！タイムラインに戻ります。");
                    return true;
                    }
                }
            </script>
        </div>
    </form>
</div>
</div>

{% endblock %}
```

```HTML:delete.html
{% extends 'base.html' %}
{% load static %}

{% block contents %}
<div class="card">
    <div class="card-header">
      {{ post.user }}<span class = 'created_at'>{{ post.created_at }}</span> 
    </div>
    <div class="card-body">
      <p class="card-text">{{ post.text }}</p>
        <form method = 'post'>
            {% csrf_token %}
            <input type = 'submit' class = 'btn btn-primary' onclick = 'return alertFunc()' value = '削除する'/>
        </form>
        <script>
            function alertFunc() {
                var del = confirm("本当に削除しますか？");
                if (del == true) {
                    return true;
                } else {
                    location.href = '{% url 'timelines:index' %}'
                    return false;
                }
            }
        </script>
    </div>
</div>


{% endblock %}
```

```HTML:comment.html
{% extends 'base.html' %}
{% load static %}

{% block contents %}
<div class = 'form-container'>
    <div class = "form">
    <form method = 'post'>
        {% csrf_token %}
        <h2 style = 'text-align:center'>返信する</h2>
        <div class = 'form-area'>
            <div class = 'userinfo'>
                <div class = 'info'>
                <label for = 'Username'><span>必須</span>返信内容を入力してください</label>
                    <div class="mb-3">
                        {{ form.text }}
                    </div>
                </div>
            </div>
            <input type = 'submit' value = '返信する' />
        </div>
    </div>
</div>
    

{% endblock %}
```

```HTML:userpost.html
{% extends 'base.html' %}
{% load static %}

{% block contents %}

{% for post in object_list %}
<div class="card">
    <div class="card-header">
      {{ post.user }}<span class = 'created_at'>{{ post.created_at }}</span> 
    </div>
    <div class="card-body">
        <p class="card-text">{{ post.text|truncatechars:30 }}</p>
        <a href='{% url 'timelines:detail' post.pk %}' style="text-decoration: none;"  class="btn-auth">投稿詳細</a>
        {% if post.user == user %}
            <a href = '{% url 'timelines:delete' post.pk %}' class="btn btn-primary">削除する</a>
            <a href = '{% url 'timelines:update' post.pk %}' class="btn btn-primary">更新</a>
        {% endif %}

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

{% endblock %}
```

<h5>Staticファイルについて</h5>

ここまでで基本的な機能と動作はしますが少し味気のないサイトになるためCSSとJavaScriptを追加します。
```CSS:ststic/css/style.css
body {
    background-color: aquamarine;
    font-family: Meirio;
}

.timeline {
    width: 60%;
    margin: 30px auto;
    text-align: center;
}

.timeline h3 {
    border-bottom: 1px solid black;
}

.comment {
    margin: 20px auto;
    width: 60%;
    text-align: center;
}

.comment h5 {
    border-bottom: 1px solid black;
}

.card {
    width: 30%;
    margin: 20px auto;
}

.created_at {
    float: right;
}

.inputarea {
    width: 40%;
    margin: 0 auto;
}

.form {
    background:#fff;
    border-radius:6px;
    padding:20px;
    padding-top:30px;
    width:60%;
    margin:50px auto;
    box-shadow:5px 5px 0px rgba(0,0,0,.1);
}

.resetform {
    background:#fff;
    border-radius:6px;
    padding:20px;
    padding-top:30px;
    width:80%;
    margin:50px auto;
    box-shadow:5px 5px 0px rgba(0,0,0,.1);
}

.resetform h1 {
    border-bottom: 1px solid black;
}

.userinfo {
    margin-bottom: 30px;
    text-align: left;
}

.info {
    margin-bottom: 30px;
    margin-top: 20px;
}

.info span {
    background-color: rgb(218, 34, 34);
    border-radius: 5px;
    color: white;
    padding: 5px;
}

.info input {
    margin-top: 20px;
}

.btn_auth {
    text-align: center;
    width: 40%;
    height: 60px;
    border-radius: 40px;
    background-color: rgb(222, 52, 173);
    color: white;
    box-shadow: 2px 2px 0px rgb(74, 73, 73);
}

.btn_auth:active {
    top: 3px;
    box-shadow: none;
}

.tab {
    max-width: 800px;
    margin: 0 auto;
    margin-top: 60px;
}
  
.top-container h2, h5 {
    margin-top: 50px;
    text-align: center
}

.tab_menu {
    display: flex;
    align-items: flex-end; 
    justify-content: left;
    min-height: 50px; 
    padding: 0;
    margin: 0;
}
  
.tab_menu-item {
    list-style: none;
    width: 200px;
    padding: 8px 5px; 
    text-align: center;
    margin-right: 6px;
    background-color: #cdcdcd;
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
    cursor: pointer;
    transition: all .3s; 
}
  
.tab_menu-item:last-of-type {
    margin-right: 0px;
 }

.tab_menu-item.is-active {
    background-color: rgba(48, 172, 249);
    color: #ffffff;
    padding: 12px 5px;
}

.tab_panel {
    width: 100%;
}

.tab_panel-box {
    padding: 10px 30px;
    border-radius: 10px;
}

.tab_panel-box001 {
    background-color: rgb(205, 246, 246);
    display: none;
}

.tab_panel-box002 {
    background-color: rgb(249, 227, 243);
    display: none;
}

.tab_panel-box.is-show {
    display: block;
}

.btn-auth {
    text-align: center;
    width: 40%;
    height: 60px;
    border-radius: 40px;
    background-color: rgb(45, 175, 250);
    color: white;
    box-shadow: 2px 2px 0px rgb(74, 73, 73);
}

.btn-auth:hover {
    background-color: rgb(247, 100, 1);
    box-shadow: none;
}

.top-container {
    text-align: center;
}

.top-container .btn-auth {
    margin-top: 30px;
}

.form-area {
    text-align: center;
    margin: 0 auto;
    width: 60%;
}

.form-container h2 {
    border-bottom: 1px solid black;
}

.mb-3 {
    margin-top: 30px;
}

.info h5 {
    margin-bottom: 30px;
}
```
javascriptではtopページのタブメニュー実装に使用しています。
他にもテンプレート内に一部記載しておりアラートや確認メッセージに利用しています。
```JavaScript:idnex.js
const tabs = document.getElementsByClassName('tab_menu-item');

for(let i = 0; i < tabs.length; i++) {
  tabs[i].addEventListener('click', tabSwitch);
}

function tabSwitch(){
  document.getElementsByClassName('is-active')[0].classList.remove('is-active');
  this.classList.add('is-active');

  document.getElementsByClassName('is-show')[0].classList.remove('is-show');
  const arrayTabs = Array.prototype.slice.call(tabs);
  const index = arrayTabs.indexOf(this);
  document.getElementsByClassName('tab_panel-box')[index].classList.add('is-show');
};
```

<h2>まとめ</h2>
ここまででSNSサイトの作成は完了です。
Djangoを使えば以外に簡単にSNSサイトを作成できます。
上記コードの中でわからないこと等あればコメントで気軽に質問してください。

コード全文は[GitHub](https://github.com/TakuyaNakahara-prg/app_sns/tree/master)に記載しています参考にしてください