Django Sprint #5 汎用ビューとCRUD 前編

- [Django Sprint #0 イントロダクション](https://qiita.com/tmgauss/private/a0dbc8b90fd5c95732af)
- [Django Sprint #1 環境構築](https://qiita.com/tmgauss/private/7e07b572dc6913475f86)
- [Django Sprint #2 新規プロジェクトのスタート](https://qiita.com/tmgauss/private/96c74381b52da4679d19)
- [Django Sprint #3 ユーザーモデルのカスタマイズ](https://qiita.com/tmgauss/private/b8edaf0da8cb8c22b1b5)
- [Django Sprint #4 トップページの作成](https://qiita.com/tmgauss/private/889793b2d7c6c89d6863)
- [Django Sprint #5 汎用ビューとCRUD 前編](https://qiita.com/tmgauss/private/0664cb98ca95e9737091)
- [Django Sprint #6 汎用ビューとCRUD 後編](https://qiita.com/tmgauss/private/96613b26dc0eea28d3c6)


## 目次

1. 汎用ビューとCRUD
1. モデルフォーム
1. ユーザーモデルに対する実装①：ログインとサインアップ



## 汎用ビューとCRUD
さて、 #4 でも紹介しましたが、汎用ビュー（generic view）はとてもパワフルなものです。「これこそがDjangoを採用した理由だ」と言っても過言ではありません。以下では、汎用ビューでできること、また、実際に動的なサイトを構築する上でどのようなプロセスを踏めば良いのかについて見ていきましょう。

#### 静的なページと動的なページ
トップページや機能の紹介のページのように、内容が変わらない限りほとんど変化しないページを静的な（static）ページと呼びます。例えば、UTokyo Project Sprintの[公式サイト](https://sites.google.com/g.ecc.u-tokyo.ac.jp/utokyo-project-sprint/top)は静的なページのみからなります。一方、データの入力・出力などを伴い、それによって表示などが変化するページを動的な（dynamic）ページと言います。

前章までで扱ったのは静的なページです。静的なページだけを作りたいなら Google sitesやWix.comのようなものを使えば十分です。しかし、動的なページを作りたいなら、本章に紹介するようなCRUDを構築する必要があります。

#### CRUDとは
CRUDとは「Create（データの作成）」「Read（データの読み込み・表示）」「Update（データの更新）」「Delete（データの削除）」の頭文字を繋いだもので、データを取り扱うソフトウェアがほとんど全て備えている基本的な機能です。

Facebookの投稿を例に見ていきましょう。まず「投稿するためにテキストや画像を書き込み（C）」、それを「タイムライン上に表示し（R）」、もし訂正したいなら「編集ボタンから内容を編集し（U）」、もし消去したいなら「投稿を削除ボタンから投稿を削除する（D）」ような流れです。

#### 汎用ビューとは
汎用ビューとは基本的な機能をゼロから書くことなく実装するためのものです。「何度も使うものは初めから用意しておこう」という理念のもと、#4で取り上げたような静的なページの出力やCRUDを簡単に実装できるように設計されています。本チュートリアルは以下の汎用ビューを取り上げます。

- [CreateView](https://docs.djangoproject.com/ja/3.0/ref/class-based-views/generic-editing/#django.views.generic.edit.CreateView)
- [ListView](https://docs.djangoproject.com/ja/3.0/ref/class-based-views/generic-display/#listview)（一覧表示）
- [DetailView](https://docs.djangoproject.com/ja/3.0/ref/class-based-views/generic-display/#detailview)（詳細表示）
- [UpdateView](https://docs.djangoproject.com/ja/3.0/ref/class-based-views/generic-editing/#django.views.generic.edit.UpdateView)
- [DeleteView](https://docs.djangoproject.com/ja/3.0/ref/class-based-views/generic-editing/#django.views.generic.edit.DeleteView)

他にも汎用ビューには種類があります。それを確認したい方は以下を参照してください。

- [Django docs | Base views](https://docs.djangoproject.com/ja/3.0/ref/class-based-views/base/)
- [Django docs | Generic display views](https://docs.djangoproject.com/ja/3.0/ref/class-based-views/generic-display/)
- [Django docs | Generic editing views](https://docs.djangoproject.com/ja/3.0/ref/class-based-views/generic-editing/)
- [Django docs | Generic date views](https://docs.djangoproject.com/ja/3.0/ref/class-based-views/generic-date-based/)
- [『Djangoの 汎用クラスビューをまとめて、実装について言及する』](https://qiita.com/renjikari/items/af3e8958d2653e6f8d46)



## モデルフォーム

#### フォームとは
ユーザーがデータを出し入れするためには、テンプレート上でフォームを表示する必要があります。例えば、ユーザー登録の際にはメールアドレスやパスワードなどを入力することになりますが、それを入力するのはフォームです。

#### GETとPOST
HTTP（Hypertext Transfer Protocol）とはWebブラウザがWebサーバと通信する際に主として使用する通信プロトコル（ルールみたいなもの）のことです。

HTTP通信は基本的には「XXをして欲しいです」（HTTPリクエスト）と「それではYYを渡します」（HTTPレスポンス）の二つからなります。そのやりとりで「どのように」して欲しいのかを規定するのがHTTPメソッドで、現在主に使われているのはGETとPOSTの2種類です。

少し分かりにくいかもしれませんが、不正確を承知で大雑把に言うと

- GET : 通常のやりとりではこちらを使う
- POST : データのやりとりがある場合はこちらを使う

との認識で当面十分だと思います。（実際にProgateもそのように説明してたはず...ここまで読んできた方はお気づきかもしれませんが、実際にはほとんどの作業を汎用ビューに任せるので。）

詳しく知りたい人は以下の記事を参照してください。

- [Wikipedia | Hypertext Transfer Protocol](https://ja.wikipedia.org/wiki/Hypertext_Transfer_Protocol#%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)
- [『GETとPOSTの違いについて』](https://qiita.com/kanataxa/items/522efb74421255f0e0a1)

#### モデルフォーム
フォームを使ってデータのやりとりを行うには

1. ユーザーがフォームにデータを入力
2. その内容をHTTPを用いてサーバー側に送信
3. その内容をビューで整形
4. モデルを通じてデータベースに反映

という過程を辿りますが、この3と4をスクラッチで書くのははっきり言って面倒です。そこで、DjangoではModelFormというものを用意しており、これを使うとほとんど3と4を勝手に実行してくれます。


#### CSRF対策
データのやりとりには常にセキュリティ上の問題が付き纏います。全てのハッキングを想定するのは不可能ですが、代表的なものに関しては十分に対策しておく必要があります。

DjangoではCSRF（Cross Site Request Forgeries）という攻撃に対して、簡単に扱える対策を提供しています。具体的な実装（コード1行）についてはこのあと触れますが、フォームには必ずCSRF対策を施すべきと記憶しておきましょう。

- [Django docs | クロスサイトリクエストフォージェリ (CSRF) 対策](https://docs.djangoproject.com/ja/3.0/ref/csrf/)



## ユーザーモデルに対する実装①：ログインとサインアップ
さあ、汎用ビューとモデルフォーム、デフォルトの認証機能を使ってログイン、ログアウト、ユーザーの作成（サインアップ）、ユーザー情報の更新、ユーザーの詳細ページ、ユーザー一覧、ユーザーの削除を実装していきましょう。

#### forms.py
フォームは新しいファイルforms.pyにまとめます。このファイルは今ないので作成しましょう。

```
code
├─ cms
│  ├─ forms.py
...
```

#### 動的ページの作成フロー
動的なページの作成の具体的な手順は以下の通りです。

1. models.pyの書き換え → マイグレーション
1. urls.pyを編集
1. forms.pyを編集
1. views.pyを編集
1. HTMLファイルを編集


#### ログイン
まずはログイン機能を実装します。この場合モデルを編集しないので、urls.pyの編集から始めましょう。

```python:/cms/urls.py
...
urlpatterns = [
    path('', views.TopView.as_view(), name='top'),
    path('login/', views.Login.as_view(), name='login'),
    # path('logout/', views.Logout.as_view(), name='logout'),
    # path('signup/', views.UserCreate.as_view(), name='signup'),
]
```
後から使うものも面倒なので、コメントアウトして書いておきました。次に、forms.pyです。AuthenticationFormというモデルフォームを使います。

```python:/cms/forms.py
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import AuthenticationForm

UserModel = get_user_model()


class LoginForm(AuthenticationForm):
    def __init__(self, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self.fields['username'].widget.attrs['class'] = 'input'
       self.fields['password'].widget.attrs['class'] = 'input'
```
そのままAuthenticationFormを使わずにそれを継承したLoginFormを使う理由はBulmaを使うためです。Bulmaではタグに```class```属性を書き加えていくことでスタイルを実装します。しかし、Djangoのテンプレートではフォームの実装に```<input>```タグ（フォーム用の標準タグ）を使わないので、```class```属性をHTMLファイル上で与えられないのです。そこで、上のように```__init__```関数上で属性を与えています。（実際にレンダリングされるときには```<input>```の形で出てきます。）

その次はviews.pyです。ここでは用意されたLoginViewを使います。

```python:/cms/views.py
from django.contrib.auth import get_user_model
from django.contrib.auth.views import (
    LoginView,
)
from django.views.generic.base import TemplateView

from .forms import (
    LoginForm,
)

UserModel = get_user_model()


class TopView(TemplateView):
    template_name = 'cms/top.html'


class Login(LoginView):
    form_class = LoginForm
    template_name = 'cms/login.html'
```
変数form_classには使用するフォームを、変数template_nameには使用するテンプレートへのパス名を指定します。

最後にテンプレート（HTML）を書いていきましょう。新しくlogin.htmlを作成します。base.htmlを引き継いで実装します。

```/cms/templates/cms/login.html
{% extends 'cms/base.html' %}

{% block title %}Login | {% endblock %}

{% block content %}
<section class="section">
    <div class="container is-mobile">
        <h1 class="title is-4">ログイン</h1>
        <h2 class="subtitle is-6">Login</h2>
        <hr>
        <p class="help">アカウントをお持ちでない方へ：サインアップは<a href="#">こちら</a></p>
        <br>
        <form method="post" action="">
            {% csrf_token %}
            <div class="field">
                <label class="label">ユーザーネーム</label>
                <div class="control has-icons-left">
                    {{ form.username }}
                    <span class="icon is-small is-left"><i class="fas fa-user"></i></span>
                </div>
            </div>
            <div class="field">
                <label class="label">パスワード</label>
                <div class="control has-icons-left">
                    {{ form.password }}
                    <span class="icon is-small is-left"><i class="fas fa-lock"></i></span>
                </div>
            </div>
            <div class="field">
                <p class="control">
                    <input type="submit" value="ログイン" class="button is-success">
                    <input type="hidden" name="next" value="{{ next }}" >
                </p>
            </div>
        </form>
    </div>
</section>
{% endblock %}
```
このようにテンプレート上でpythonのコードを書きたい場合は```{{ }}```や```{% %}```を使います。当面、前者は実際に表示されるもの、後者は表示されないものと区別しておけば大丈夫です。フォームの型については、

- [Bulma | Form](https://bulma.io/documentation/form/)

を参考にしてください。また、```<form>```タグの直下に```{% csrf_token %}```を書くことを忘れないでください。これは先に述べたCSRF対策のためのもので、全てのフォームでこれを書くべきです。

本来ならこれでOKですが、ログイン機能の実装には十分ではありません。settings.pyにログイン系のURLを伝えなければなりません。

```/config/settings.py
...
# Custom
AUTH_USER_MODEL = 'cms.User'
LOGIN_URL = 'cms:login'
LOGIN_REDIRECT_URL = 'cms:top'
```
url関数に渡せるような形```'(アプリ名):(urls.pyのname)'```であれば大丈夫です。ここではひとまず、ログイン直後にトップページに飛ぶようにしています。


#### ログアウト
ログアウトも簡単です。上記の要領で下のように書き換えてください。ログアウトはボタンをクリックするだけなので、フォームもテンプレートも
必要ありません。

```python:/cms/urls.py
...
urlpatterns = [
    path('', views.TopView.as_view(), name='top'),
    path('login/', views.Login.as_view(), name='login'),
    path('logout/', views.Logout.as_view(), name='logout'),
    # path('signup/', views.UserCreate.as_view(), name='signup'),
]
```

```/cms/views.py
...
from django.contrib.auth.views import (
    LoginView, LogoutView,
)
...
class Logout(LogoutView):
    pass
```

```/config/settings.py
...
LOGOUT_REDIRECT_URL = 'cms:top'
```

#### ユーザーの作成（サインアップ）
ユーザーモデルの作成には汎用ビューとしてCreateViewを、モデルフォーム（厳密には特殊なモデルフォームと言える）としてUserCreationViewを使います。具体的な流れは上と同様です。

```python:/cms/urls.py
...
urlpatterns = [
    path('', views.TopView.as_view(), name='top'),
    path('login/', views.Login.as_view(), name='login'),
    path('logout/', views.Logout.as_view(), name='logout'),
    path('signup/', views.UserCreate.as_view(), name='signup'),
]
```

```python:/cms/forms.py
...
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
...
class UserCreateForm(UserCreationForm):
    class Meta:
        model = UserModel
        fields = ('username', 'email', 'password1', 'password2')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'input'
```

```python:/cms/views.py
from django.contrib.auth import get_user_model, login
from django.contrib.auth.views import (
    LoginView, LogoutView,
)
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    CreateView,
)

from .forms import (
    LoginForm, UserCreateForm,
)
...
class UserCreate(CreateView):
    form_class = UserCreateForm
    template_name = 'cms/signup.html'
    success_url = reverse_lazy('cms:top')

    def form_valid(self, form):
        user = form.save()
        login(self.request, user)
        self.object = user
        return HttpResponseRedirect(self.get_success_url())
```

```/cms/templates/cms/signup.html
{% extends 'cms/base.html' %}

{% block title %}Signup | {% endblock %}

{% block content %}
<section class="section">
    <div class="container is-mobile">
        <h1 class="title is-4">サインアップ</h1>
        <h2 class="subtitle is-6">Sign up</h2>
        <hr>
        <p class="help">アカウントをお持ちの方へ：ログインは<a href="#">こちら</a></p>
        <br>
        <form method="post" action="">
            {% csrf_token %}
            <div class="field">
                <label class="label">ユーザーネーム</label>
                <div class="control has-icons-left">
                    {{ form.username }}
                    <span class="icon is-small is-left"><i class="fas fa-user"></i></span>
                </div>
            </div>
            <div class="field">
                <label class="label">メールアドレス</label>
                <div class="control has-icons-left">
                    {{ form.email }}
                    <span class="icon is-small is-left"><i class="fas fa-envelope"></i></span>
                </div>
            </div>
            <div class="field">
                <label class="label">パスワード</label>
                <div class="control has-icons-left">
                    {{ form.password1 }}
                    <span class="icon is-small is-left"><i class="fas fa-lock"></i></span>
                </div>
            </div>
            <div class="field">
                <label class="label">パスワード（確認）</label>
                <div class="control has-icons-left">
                    {{ form.password2 }}
                    <span class="icon is-small is-left"><i class="fas fa-lock"></i></span>
                </div>
            </div>
            <div class="field">
                <p class="control">
                    <input type="submit" value="登録" class="button is-success">
                    <input type="hidden" name="next" value="{{ next }}" >
                </p>
            </div>
        </form>
    </div>
</section>
{% endblock %}
```


#### エラーメッセージの確認
エラーに対して適切なメッセージを出力することはWeb構築において欠かせません。例えば、ログインに失敗したときに元のページに戻ってきても、ユーザーはなぜ戻ってきたのか分からず、同じミスを繰り返すことになります。エラーメッセージを出力するように、base.htmlを書き換えましょう。

```/cms/templates/cms/base.html
...
<!-- MESSAGE -->
{% for field, errors in form.errors.items %}
    <article class="message is-warning">
        <div class="message-body">
        {% for error in errors %}
            <p><strong>{{ error }}</strong></p>
        {% endfor %}
        </div>
    </article>
{% endfor %}
<!-- MAIN -->
...
```

#### ログイン・アウトによるテンプレートの切替
ログイン前後でナビゲーションバーに書くべき内容は変わります。例えば、ログイン後のユーザーにはサインアップボタンは必要ありません。そこで、ログインユーザーとそれ以外で条件分岐させます。具体的にはユーザーモデルにある```is_authenticated```メソッドを使います。

```/cms/templates/cms/base.html
...
<div class="navbar-end">
    {% if user.is_authenticated %}
    <a class="navbar-item" href="{% url 'cms:logout' %}">Log out</a>
    {% else %}
    <a class="navbar-item is-grey" href="#">About</a>
    <div class="buttons">
        <a class="button is-info navbar-item" href="{% url 'cms:signup' %}"><strong>Sign up</strong></a>
        <a class="button is-light navbar-item" href="{% url 'cms:login' %}"><strong>Log in</strong></a>
    </div>
    {% endif %}
</div>
...
```


#### 最後の仕上げ
最後の仕上げに```<a>```タグのリンクを設定しましょう。

```/cms/templates/cms/login.html
...
<p class="help">アカウントをお持ちでない方へ：サインアップは<a href="{% url 'cms:signup' %}">こちら</a></p>
...
```

```/cms/templates/cms/signup.html
...
<p class="help">アカウントをお持ちの方へ：ログインは<a href="{% url 'cms:login' %}">こちら</a></p>
...
```
一度ユーザーを作成し、挙動を確認してみましょう。また、管理者画面できちんとユーザーが作成されていることを確認しましょう。

#### テスト
実際にはテストのためのコードを書いて確認を行う必要があります。しかし、本チュートリアルでは学習コストなどを鑑みて扱っておりません。もし興味のある方は

- [Django docs | Django におけるテスト](https://docs.djangoproject.com/ja/3.0/topics/testing/)
- [Django docs | テストを書いて実行する](https://docs.djangoproject.com/ja/3.0/topics/testing/overview/)
- [Django docs | テストツール](https://docs.djangoproject.com/ja/3.0/topics/testing/tools/)


## 参照

- [Django Sprint #0 イントロダクション](https://qiita.com/tmgauss/private/a0dbc8b90fd5c95732af)
- [Django Sprint #1 環境構築](https://qiita.com/tmgauss/private/7e07b572dc6913475f86)
- [Django Sprint #2 新規プロジェクトのスタート](https://qiita.com/tmgauss/private/96c74381b52da4679d19)
- [Django Sprint #3 ユーザーモデルのカスタマイズ](https://qiita.com/tmgauss/private/b8edaf0da8cb8c22b1b5)
- [Django Sprint #4 トップページの作成](https://qiita.com/tmgauss/private/889793b2d7c6c89d6863)
- [Django Sprint #5 汎用ビューとCRUD 前編](https://qiita.com/tmgauss/private/0664cb98ca95e9737091)
- [Django Sprint #6 汎用ビューとCRUD 後編](https://qiita.com/tmgauss/private/96613b26dc0eea28d3c6)
- [Django Sprint Appendix Docker関連](https://qiita.com/tmgauss/private/bf3545d17c7271ed4fcb)
- [Django Sprint Appendix 各種実装まとめ](https://qiita.com/tmgauss/private/f92044b346825b812121)
- [Django Sprint Appendix モデルとデータベース](https://qiita.com/tmgauss/private/5e882e71100b798209d8)
- [Django+PostgreSQLのアプリケーションをAWSのElastic Beanstalkにデプロイする (UTokyo Project Sprint 用)](https://qiita.com/Chrg/private/d0c6fa829040613d1bb6)
- [Django+MySQLのアプリケーションをAWSのElastic Beanstalkにデプロイする (UTokyo Project Sprint 用)](https://qiita.com/yulingo-ringo/private/5c2be0a31636c33565f1)