Djangoチュートリアル（ブログアプリ作成）⑤ - 記事作成機能編


前回、[Django チュートリアル（ブログアプリ作成）④ - ユニットテスト編](https://qiita.com/tmasuyama/items/cddbabbb566c00f54c89) ではユニットテストの実装方法を学んでいきました。

本来であれば、せっかくなので期待するテストを先に書くテスト駆動開発スタイルで実装していきたいところですが
このチュートリアルでは Django ではどんなことが出来るかを学びながらなので、先に実装してからユニットテストを書いていきます。

これから、何回かに分けて今回は以下の機能を追加していきます。

1.記事の作成 (Create) 2.記事の詳細 (Read) 3.記事の編集 (Update) 4.記事の削除 (Delete)

これら機能の頭文字をとって「CRUD」とここでは呼ぶことにします。
(厳密にいえば既に blog_list で Read は出来るので違うかもしれませんが）

さて、今回はベースともいえる**記事の作成**機能を追加していきましょう。
これまでは管理サイトを使って superuser 　権限で記事を追加していましたが、アプリ内で記事を作成出来たほうが便利ですよね。

## form の準備

アプリからデータを追加するときは form という仕組みを使います。
form がユーザから入力データを受け付け、view を通して model へデータを渡してデータベースへ登録することができるようになります。

まずは blog アプリ配下に forms.py というファイルを作成します。

```
.
├── blog
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py # 追加
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   ├── tests
│   │   ├── __init__.py
│   │   ├── test_forms.py
│   │   ├── test_models.py
│   │   ├── test_urls.py
│   │   └── test_views.py
│   ├── urls.py
│   └── views.py
├── db.sqlite3
├── manage.py
├── mysite
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── templates
    └── blog
        ├── index.html
        └── post_list.html
```

作成した forms.py の中身はこのようになります。

```python:forms.py
from django import forms
from .models import Post

class PostCreateForm(forms.ModelForm): # DjangoのModelFormでは強力なValidationを使える

    class Meta:
        model = Post # Post モデルと接続し、Post モデルの内容に応じてformを作ってくれる
        fields = ('title', 'text') # 入力するカラムを指定
```

説明文も入れているのである程度わかるかと思います。
データを投入したい model を指定すると form が対応する入力フォームを用意してくれるようになります。

なお、最終行でデータを入力するカラムをフィールドとして指定していますが、
**fields = '**all**'** と定義することで全てのカラムを手入力するように指定することもできます。

## urls.py の修正

新たに view に追加するクラスは \*\*PostCreateView と名前を予め決めておき、
urls.py では次のようにルーティングを追加しておきましょう。

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

app_name = 'blog'

urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('post_list', views.PostListView.as_view(), name='post_list'),
    path('post_create', views.PostCreateView.as_view(), name='post_create'), # 追加
]
```

success_url では、データベースの変更に成功した場合にリダイレクトさせるページを指定しています。
「アプリ名：逆引き URL 名」の形で指定し、結果的に urls.py で指定した 'post_list' にリダイレクトされることになります。
※reverse_lazy は、view に対応した「URL の文字列」を返す。今回であれば /blog/post_list を返してくれます

## views.py の修正

先ほど追加するクラス名は PostCreateView と決めましたね。
また、form で入力フォーム用のクラスも作成しました。

次は views.py の中で、またまた汎用クラスビューを使ってクラスを作成します。

```python:views.py
from django.views import generic
from django.urls import reverse_lazy
from .forms import PostCreateForm # forms.py で作ったクラスをimport
from .models import Post

class IndexView(generic.TemplateView):
    template_name = 'blog/index.html'

class PostListView(generic.ListView):
    model = Post

class PostCreateView(generic.CreateView): # 追加
    model = Post # 作成したい model を指定
    form_class = PostCreateForm # 作成した form クラスを指定
    success_url = reverse_lazy('blog:post_list') # 記事作成に成功した時のリダイレクト先を指定
```

Django の強力なクラスベース汎用ビューのおかげで、これだけのコードで済みます。
作成したい model、作成した form クラス、そして記事作成が成功した時のリダイレクト先を指定してあげるだけです。

## template の準備

まだ記事作成（投稿）用の html は作成していなかったので、templates/blog 配下に作成してあげましょう。
名前は post_form.html とします。
※クラスベース汎用ビューの命名規則に沿うことで template 名を指定しなくても OK になります

```
└── templates
    └── blog
        ├── index.html
        ├── post_form.html # 追加
        └── post_list.html
```

```html:post_create.html
<form action="" method="POST">
  <table class="table">
      <tr>
        <th>タイトル</th>
        <td>{{ form.title }}</td>
      </tr>
      <tr>
        <th>本文</th>
        <td>{{ form.text }}</td>
      </tr>
  </table>
  <button type="submit" class="btn btn-primary">送信</button>
  {% csrf_token %}
</form>
```

forms.py の fields で指定した入力フィールドを form という変数で受け取ることができるので、
template 側では form.title と form.text という形で取り出すことができます。

また CSRF (クロスサイトリクエストフォージェリ) という、入力フォームを利用した攻撃を防ぐためのものです。
入力フォームを html に表示させる時は、必ず入れておきましょう。

この状態で runserver コマンドでサーバを起動すると、見た目はひどいものですが入力フォームが表示されます。
（度々ですが、まずは Django の基本をおさえてから見た目を整えます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/131ef05c-ae3c-987b-00ad-421b792c6bd0.png)

では試しに値を入力し、送信してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/85d81115-ffb6-6ffa-531a-70daa892fddd.png)

送信を押すと、post_list にリダイレクトされます。
そして、先ほど投稿した記事が追加されていることが分かるかと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/787564b3-747b-5295-e0d8-280b2b404ab2.png)

これで無事に記事投稿機能を追加することができました。

## test_views.py の修正

最後に今回の機能のテストを実装しましょう。

test_views.py に下記のテストクラスを作成します。

```python:test_views.py
...
class PostCreateTests(TestCase):
    """PostCreateビューのテストクラス."""

    def test_get(self):
        """GET メソッドでアクセスしてステータスコード200を返されることを確認"""
        response = self.client.get(reverse('blog:post_create'))
        self.assertEqual(response.status_code, 200)

    def test_post_with_data(self):
        """適当なデータで　POST すると、成功してリダイレクトされることを確認"""
        data = {
            'title': 'test_title',
            'text': 'test_text',
        }
        response = self.client.post(reverse('blog:post_create'), data=data)
        self.assertEqual(response.status_code, 302)

    def test_post_null(self):
        """空のデータで POST を行うとリダイレクトも無く 200 だけ返されることを確認"""
        data = {}
        response = self.client.post(reverse('blog:post_create'), data=data)
        self.assertEqual(response.status_code, 200)
```

基本となる GET の確認、適当なデータの投入とリダイレクトの確認、そして空データ投入時のレスポンスを見ています。

最後にテストを実行しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/96d72a40-404a-aaa8-5ed5-ec1b082bc424.png)

無事に通りましたね。

次回は一気に記事の詳細画面を作成していきます。

→ 次回
[Django チュートリアル（ブログアプリ作成）⑥ - 記事詳細・編集・削除機能編](https://qiita.com/tmasuyama/items/32a1ea8fe5c2f1018112)