Python初心者によるDjango超入門！その４ 超シンプルな日記アプリケーションを作ってみた（クラスベース汎用ビューを利用しないで関数のみで作成）

# 本記事について
UdemyでDjangoについて学習した結果のアウトプットページです。
前の記事は<a href=https://qiita.com/sw1394/items/4bc6349dd3a32938dcaf>こちら</a>です。

今回は、Djangoで超シンプルな日記アプリケーションを作成したいと思います。
また、今回はDjangoの動作について理解を深めるためにクラスベース汎用ビューを利用せず、関数のみで作ります。

# 成果物
トップページ
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242659/df28e108-b7ef-11af-1966-2d090454b3b9.png">

詳細ページ
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242659/19f148c3-4535-6801-3965-db99e6401c80.png">

更新ページ
<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242659/7cd8a903-8f00-d60d-821e-df62aaa45423.png">

削除ページ
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242659/a0760dda-ca2f-5902-bee9-763cfd6cb364.png">

# 事前準備
まずは環境構築から。
Secondというプロジェクトを作成し、diaryというDjangoアプリを作成します。
プロジェクト名は任意ですが、可能なら半角英数字のみにしておいた方が無難です。
アンダーバーや空白スペースがプロジェクト名にあると後述の手順で謎のエラーが発生することがあります。

```PowerShell
django-admin startproject Second
cd Second
py .\manage.py startapp diary
```

settings.pyにdiaryアプリを登録して、日本時間をセット

```Second\Second\sattings.py
INSTALLED_APPS = [
    'diary',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'
```

# models.py
今回はDjangoのDBにデータを登録する処理が発生するので、models.pyでDBにどんなデータを入れるのか定義します。
逆に言うと、models.pyを見れば、DjangoのDBにどんなデータフィールドが用意されているのか分かるということです。
また、models.pyで明示的に主キーを設定しなかった場合、idという主キーが自動でセットされます。

```Second\Second\models.py
from django.db import models
from django.utils import timezone

class Day(models.Model):
    #ここで宣言はしていないけど、idという主キーが自動で登録されている
    title = models.CharField('タイトル', max_length=200)
    text = models.TextField('本文')
    date = models.DateField('日付', default=timezone.now)
```

models.pyの入力が終わったら、manage.pyでDBに登録します。
manage.pyが保存されているフォルダに移動して、以下のコマンドで登録してください。

```PowerShell
py .\manage.py makemigrations
py .\manage.py migrate
```

makemigrations実行時に、RecursionErrorが表示されることがあります。
``RecursionError: maximum recursion depth exceeded while calling a Python object``

エラーの原因の１つとして、プロジェクト名に半角英数字以外の文字があったり、
パスの中に空白スペースが含まれている場合に発生することがあります。
OneDirveなどの特殊なフォルダに保存していると空白スペースがパスに含まれることがありますので、注意してください。


# urls.py
プロジェクト直下にあるurls.pyで、アプリ直下にあるurls.pyに行くように設定します。

```Second\Second\urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('diary/', include('diary.urls')),
]
```

アプリ直下にあるurls.pyを以下のように記入。

```Second\diary\urls.py
from django.urls import path
from . import views

app_name = 'diary'

urlpatterns = [
    path('', views.index, name='index'), #一覧ページ
    path('add/', views.add, name='add'), #追加ページ
    path('update/<int:pk>/', views.update, name='update'), #更新ページ
    path('delete/<int:pk>', views.delete, name='delete'), #削除ページ
    path('detail/<int:pk>', views.detail, name='detail'), #詳細ページ
]
```

アプリ直下のurls.pyにはURLを登録。
今回は一覧、追加、更新、削除、詳細の５ページを作成します。
``<int:pk>``というのは、それぞれの記事に紐づいている主キーという意味です。

models.pyの説明時にidという主キーが自動登録されていると書きましたが、
Djangoではそのidをpkという変数で表現することが慣例となっています。
``<int:id>``と表現しても良いのですが、``<int:pk>``としておきましょう。

例えば、pkが1の詳細ページにアクセスする場合は、``diary/detail/1/``になります。
特定の記事に紐づいていない、一覧ページや追加ページには不要です。

# forms.py
views.pyの作成の前に、forms.pyというファイルを新規に作成します。
views.pyと同じフォルダ（Second\diary）にforms.pyというファイルを作成し、
内容を以下のようにしてください。

```Second\diary\forms.py
from django import forms
from .models import Day


class DayCreateForm(forms.ModelForm):

    class Meta:
        model = Day
        fields = '__all__'
```

記事を作成したり、更新したりする場合、当然ですがHTMLファイルに入力欄が必要になります。
その入力欄を作成するにあたり、HTMLファイルにFormというものを構成する必要があります。

そのFormに対して、どんなデータを渡すのかを定義するのがこのforms.pyです。
今回はmodels.pyで定義されているDayクラスの全てのデータを渡すことにしましょう。
全てを渡す場合は``fields = '__all__'``としてください。

DayCreateFormというクラス名は任意ですが、
その中にあるMetaというクラスは定型文です。
変更しないようにしましょう。

# views.py
views.pyに一覧、追加、更新、削除、詳細のページを表示するための関数を作成します。
本来であれば、クラスベース汎用ビューというDjangoの神機能を使うことで簡単に書けるのですが、
今回の記事は関数で書いてみようというものなので、あえて関数で書きます。

```Second\diary\views.py
from django.shortcuts import render, redirect, get_object_or_404
from .forms import DayCreateForm
from .models import Day

def index(request):
    """
    日記の一覧を表示
    """
    context = {
        'day_list':Day.objects.all(),
    }
    return render(request, 'diary/day_index.html', context)


def add(request):
    """
    日記の記事を追加
    """
    # 送信内容を元にフォームを作る。POSTじゃなければ空のフォームを作成。
    form = DayCreateForm(request.POST or None)

    # method==POSTとは送信ボタンが押されたとき。form.is_validは入力内容に問題が無い場合Trueになる。
    if request.method == 'POST' and form.is_valid():
        form.save()
        return redirect('diary:index')
    
    # 通常時のアクセスや入力内容に誤りがあれば、再度day_form.htmlを表示
    context = {
        'form':form
    }
    return render(request, 'diary/day_form.html', context)


def update(request, pk):
    """
    日記の記事を変更
    """
    # urlのpkを基に、Dayを取得（pkはidと同じ）
    day = get_object_or_404(Day, pk=pk)

    # formに取得したDayを紐づける
    form = DayCreateForm(request.POST or None, instance=day)

    # method=POST(送信ボタン押下)、でかつ、入力内容に問題がなければフォームを保存する。
    if request.method == 'POST' and form.is_valid():
        form.save()
        return redirect('diary:index')
    
    # 通常時のアクセスや入力内容に問題があった場合は最初のページを表示
    context = {
        'form':form
    }
    return render(request, 'diary/day_form.html', context)


def delete(request, pk):
    """
    日記の記事を削除
    """
    # URLのPKを基に、Dayを取得（pkはidと同じ）
    day = get_object_or_404(Day, pk=pk)

    # method=POST(送信ボタン押下)
    if request.method == 'POST':
        day.delete()
        return redirect('diary:index')
    
    # 通常時のアクセス。または問題があった場合のアクセス。
    context = {
        'day':day
    }
    return render(request, 'diary/day_delete.html', context)


def detail(request, pk):
    """
    日記の詳細ページ
    """
    #　URLのPKを基に、Dayを取得
    day = get_object_or_404(Day, pk=pk)

    context = {
        'day':day
    }
    return render(request, 'diary/day_detail.html', context)
```

``def index(request):``は記事の一覧を表示するための関数です。
models.pyにあるDayクラスの全てのデータを、contextに代入します。
render関数を使って、day_index.htmlに作成したcontextを渡しています。

``def add(request):``は記事を新規作成するための関数です。
``form = DayCreateForm(request.POST or None)``で送信内容を元にフォームを作ります。送信前の状態だと、空のフォームになります。
``if request.method == 'POST' and form.is_valid():``は、送信ボタンが押され、かつ、内容に問題が無い場合の処理です。
問題が無いので、form.save()で保存して、index.htmlのページにリダイレクトします。
通常時のアクセスの場合や入力に誤りがあった場合は、render関数でday_form.htmlにcontextを渡します。

``def update(request, pk):``は記事の内容を変更するための関数です。
このupdateページを表示する場合、URLはdiary/update/<記事のpk>になります。
更新対象となる記事の主キーが必要になるので、pkを引数に渡します。
URLで受け取ったpkをDayクラスに紐づけて記事の内容を以下のコードで取得します。
``day = get_object_or_404(Day, pk=pk)``
後の処理はadd関数と同じ流れです。

``def delete(request, pk):``は記事を削除するための関数です。
ほとんどupdateと同じ処理ですが、削除なのでformを操作する必要が無いので、作成しません。
day.delete()で記事を削除し、index.htmlへリダイレクトします。

``def detail(request, pk):``は記事の詳細ページを表示するための関数です。
formも作成する必要が無いのでシンプルです。

# htmlファイルの作成
最後にhtmlファイルを作成します。
diaryフォルダの下にtemplatesフォルダを作成し、更にdiaryフォルダを作成します。
※<プロジェクト名>/<アプリ名>/templates/<アプリ名>と覚えると楽です。

### base.htmlの作成
まずbase.htmlを作成します。
すべての大本となるhtmlファイルです。
※CSSやJavaScriptはBootstrapを利用しています。

nav部分で一覧ページと記事の新規作成をリンクしておきます。
リンクは``{% url '<アプリ名>:<urls.pyで定義したnameの値>'%}``でリンクしましょう。

```Second\diary\templates\diary\base.html
<!doctype html>
<html lang="ja">
  <head>
    <title>日記アプリケーション</title>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
  </head>
  <body>

    <div class="container">
      <nav class="nav">
        <a class="nav-link active" href="{% url 'diary:index' %}">一覧</a>
        <a class="nav-link" href="{% url 'diary:add' %}">追加</a>
      </nav>
      {% block content %}
      {% endblock %}
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
  </body>
</html>
```

### day_index.htmlの作成

記事の一覧ページです。

views.pyから、day_listがKeyのcontextを渡されているので、
day_listを使ってDBに保存されている記事のタイトルと日付にアクセスできます。
これをfor文を使って表示させています。

updateとdeleteにはday.pkを渡して、それぞれの記事の更新ページへアクセスする仕組みです。

```Second\diary\templates\diary\day_index.html
{% extends 'diary/base.html' %}

{% block content %}

<h1>日記一覧</h1>

<table class="table">
    <thead>
        <tr>
            <th>タイトル</th>
            <th>日付</th>
            <th>更新処理</th>
            <th>削除処理</th>
        </tr>
    </thead>
    <tbody>
    {% for day in day_list %}
        <tr>
            <td><a href="{% url 'diary:detail' day.pk %}">{{ day.title }}</a></td>
            <td>{{ day.date }}</td>
            <td><a href="{% url 'diary:update' day.pk %}">更新</a></td>
            <td><a href="{% url 'diary:delete' day.pk %}">削除</a></td>
        </tr>
    {% endfor %}
    </tbody>
</table>

{% endblock %}
```

### day_detail.htmlの作成

詳細ページです。
title、text、dateを表示しています。
textの後に、linebreaksbrと付け足しているのは、
HTML上で改行を表現するためです。無くても問題はありません。

```Second\diary\templates\diary\day_detail.html.html
{% extends 'diary/base.html' %}
{% block content %}
    <table class="table">
        <tr>
            <th>タイトル</th>
            <td>{{ day.title }}</td>
        </tr>
        <tr>
            <th>本文</th>
            <td>{{ day.text | linebreaksbr}}</td>
        </tr>
        <tr>
            <th>日付</th>
            <td>{{ day.date }}</td>
        </tr>
    </table>
    <a href="{% url 'diary:update' day.pk %}"><button class="btn btn-primary">更新</button></a>
    <a href="{% url 'diary:delete' day.pk %}"><button class="btn btn-danger">削除</button></a>    
    
{% endblock %}
```

### day_form.htmlの作成
フォームの作成です。
記事の作成・更新で利用します。


```Second\diary\templates\diary\day_form.html
{% extends 'diary/base.html' %}

{% block content %}
<form action="" method="POST">
    <table class="tabel">
        <tr>
            <th>タイトル</th>
            <td>{{ form.title }}</td>
        </tr>
        <tr>
            <th>本文</th>
            <td>{{ form.text }}</td>
        </tr>
        <tr>
            <th>日付</th>
            <td>{{ form.date }}</td>
        </tr>
    </table>

    <button type="submit" class="btn btn-primary">送信</button>
    {% csrf_token %}
</form>
{% endblock %}
```

### day_delete.htmlの作成
似たような内容が続きますが、これで最後です。
最後は削除ページです。

```Second\diary\templates\diary\day_delete.html
{% extends 'diary/base.html' %}
{% block content %}
<form action="" method="POST">
    <table class="table">
        <tr>
            <th>タイトル</th>
            <td>{{ day.title }}</td>
        </tr>
        <tr>
            <th>本文</th>
            <td>{{ day.text }}</td>
        </tr>
        <tr>
            <th>日付</th>
            <td>{{ day.date }}</td>
        </tr>
    </table>
    <p>こちらのデータを削除します。</p>
    <button type="submit" class="btn btn-primary">削除</button>
    {% csrf_token %}
</form>
{% endblock %}

```

# 動作確認
あとはpy manage.py runserverで動作確認しましょう。
記事の作成・更新・削除が出来たら成功です。

次の記事は↓
https://qiita.com/sw1394/items/5d5af3356485c6980d92