Django Sprint #6 汎用ビューと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. 前編の復習
1. ユーザーモデルに対する実装②：ユーザー情報の更新
1. ユーザーモデルに対する実装③：ユーザーの詳細ページと一覧ページ
1. ユーザーモデルに対する実装④：ユーザーの削除


## 前編の復習
前編に引き続き、後編ではユーザーモデルのCRUDを実装していきます。動的なページの作成フローは

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

の手順でした。後編でもそれは変わりません。

## ユーザーモデルに対する実装②：ユーザー情報の更新
この章ではCRUDのUを実装します。

#### 「動的な」URL
ユーザーの更新ページはログインページなどと違って、ユーザーごとに異なります。だからと言って、ユーザーの数だけページを用意するのは愚かな話です。その割り振りはいろいろな方法が考えられますが、一番メジャーなのはURLにuserのidを埋め込み、それによってページを分けることです。cms/urls.pyを次のように書き換えましょう。

```/cms/urls.py
...
urlpatterns = [
    ...
    path('user/<int:pk>/update/', views.UserUpdate.as_view(), name='user_update'),
]
...
```

```<int:pk>```の```int```はデータの型（整数）、```pk```はprimary key（データに固有の番号など）の略でこの場合、ユーザーidを示します。（各モデルにはデフォルトでIDが振られています。）

この場合、例えば```/user/1/update/```にアクセスした場合、ユーザーIDが1であるユーザーの更新ページにつながります。

#### モデルフォーム
これまでと同様にforms.pyを編集します。ここでモデルフォーム```forms.ModelForm```を使います。使い方は簡単で```Meta```クラスにmodelとfieldsを追加するだけです。具体的には

```/cms/forms.py
from django import forms
...
class UserUpdateForm(forms.ModelForm):
    class Meta:
        model = UserModel
        fields = ('username', 'first_name', 'last_name', 'email')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'input'
...
```
のような形です。この場合ユーザーネーム、姓名、メールアドレスを更新できるようになっています。


#### アクセス権限
更新ページにアクセスできるのは「ログインユーザー」かつ「更新ページのidを持つユーザー」でなければなりません。例えば、```user/3/update/```に```user.id == 4```のユーザーがアクセスできたら大問題です。（このような場合403エラーを返さなければなりません。）そこで、このような認証機能を実装しなければなりません。この機能はよく使うのでmixins.pyという別のファイルにまとめておきましょう。cms直下にmixins.pyを作ります。

```/cms/mixins.py
from django.contrib.auth.mixins import UserPassesTestMixin


class OnlyYouMixin(UserPassesTestMixin):
    raise_exception = True

    def test_func(self):
        user = self.request.user
        return user.pk == self.kwargs['pk'] or user.is_superuser
```
UserPassesTestMixinではtest_funcの出力がFalseのときに強制的に403エラーを吐かせることができます。（他にmixins.pyを使うときは、例えばグループのメンバーだけをアクセス許可したい時などです。）ここでは、管理者にも権限を与えています。

あとは、views.pyを書き換えましょう。先ほどのOnlyYouMixinを読み込むのを忘れないようにしましょう。

```/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.shortcuts import resolve_url
from django.urls import reverse_lazy
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    CreateView, UpdateView,
)

from .mixins import OnlyYouMixin
from .forms import (
    LoginForm, UserCreateForm, UserUpdateForm,
)
...
class UserUpdate(OnlyYouMixin, UpdateView):
    model = UserModel
    form_class = UserUpdateForm
    template_name = 'cms/user_update.html'

    def get_success_url(self):
        return resolve_url('cms:user_detail', pk=self.kwargs['pk'])
```

#### テンプレート
あとは、テンプレートを書き換えるだけです。

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

{% block title %}{{ user.username }} | {% endblock %}

{% block content %}
<section class="section">
    <div class="container is-mobile">
        <h1 class="title is-4">ユーザー情報の更新</h1>
        <h2 class="subtitle is-6">Update your information</h2>
        <hr>
        <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.last_name }}
                    <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.first_name }}
                    <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">
                <p class="control">
                    <input type="submit" value="保存" class="button is-success">
                    <input type="hidden" name="next" value="{{ next }}" >
                </p>
            </div>
        </form>
    </div>
</section>
{% endblock %}
```
一度ブラウザ上でアクセスして確認してみましょう。





## ユーザーモデルに対する実装③：ユーザーの詳細ページと一覧ページ
次はCRUDのRを実装します。

## ユーザーの詳細ページ
DetailViewを使います。urls.py、views.py、user_detail.html（user_detail.htmlは新規作成）を編集します。もしログインユーザーに限定したいなら、UserDetailビューにLoginRequiredMixinも合わせて継承させましょう。

```/cms/urls.py
...
urlpatterns = [
    ...
    path('user/<int:pk>/', views.UserDetail.as_view(), name='user_detail'),
]
...
```

```/cms/views.py
...
from django.views.generic.detail import DetailView
...
class UserDetail(DetailView):
    model = UserModel
    template_name = 'cms/user_detail.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['pk'] = self.kwargs['pk']
        return context
```

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

{% block title %}{{ user.last_name }} {{ user.first_name }} | {% endblock %}

{% block hero %}
<section class="hero is-link is-bold">
    <div class="hero-body">
        <div class="container">
            <div class="media">
                <div class="media-left">
                    <figure class="image is-64x64 is-left">
                        <img class="is-rounded" src="https://bulma.io/images/placeholders/128x128.png">
                    </figure>
                </div>
                <div class="media-content">
                    <h1 class="title">
                        {{ user.last_name }} {{ user.first_name }}
                    </h1>
                    <h2 class="subtitle">
                        {{ user.username }}
                    </h2>
                    {% if user.pk == request.user.pk %}
                    <a href="{% url 'cms:user_update' user.pk %}">
                        <button class="button is-small is-info is-inverted is-outlined">
                            <span class="icon is-small is-left"><i class="fas fa-user-edit"></i></span>
                            <strong>プロフィールを追加する</strong>
                        </button>
                    </a>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
</section>
{% endblock %}

{% block content %}
<section class="section">
    <div class="container">
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ornare magna eros, eu pellentesque tortor vestibulum ut. Maecenas non massa sem. Etiam finibus odio quis feugiat facilisis.</p>
    </div>
</section>
{% endblock %}
```
これで自分の詳細ページでは更新ページへのリンクが表示されています。

#### ユーザー一覧
ListViewを使います。urls.py、views.py、user_list.html（user_list.htmlは新規作成）を編集します。


```/cms/urls.py
...
urlpatterns = [
    ...
    path('user/', views.UserList.as_view(), name='user_list'),
]
...
```

```/cms/views.py
...
from django.views.generic.list import ListView
...
class UserList(ListView):
    model = UserModel
    template_name = 'cms/user_list.html'
```

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

{% block title %}ユーザー一覧 | {% endblock %}

{% block hero %}
<section class="hero is-small is-link is-bold">
    <div class="hero-body">
        <div class="container has-text-centered">
            <h1 class="title">ユーザー一覧</h1>
            <h2 class="subtitle">User List</h2>
        </div>
    </div>
</section>
{% endblock %}

{% block content %}
<section class="section">
    <div class="container">
        <div class="tile is-ancestor">
            <div class="tile is-12 is-vertical is-parent">
                {% for object in object_list %}
                <div class="tile is-child box">
                    <a class="title is-4" href="{% url 'cms:user_detail' object.id %}">{{ object.username }}</a>
                    <p>Lorem ipsum dolor sit</p>
                </div>
                {% endfor %}
            </div>
        </div>
    </div>
</section>
{% endblock %}
```


#### ユーザーモデルの書き換え
ユーザーモデルに「twitter」というフィールド（カラム）を追加してみましょう。まず、models.pyを編集して、ユーザーモデルにtwitterフィールドを追加します。

```python:/cms/models.py
...
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

twitter = models.CharField(_('Twitter'), max_length=50, blank=True)
...
```
次にこれをDBに反映させるためにマイグレーションしなければなりません。マイグレーションは次の二つのコマンドでした。

```terminal
$ docker-compose run --rm web python manage.py makemigrations 
Starting code_db_1 ... done
Migrations for 'cms':
  cms/migrations/0002_user_twitter.py
    - Add field twitter to user
```

```terminal
$ docker-compose run --rm web python manage.py migrate
Starting code_db_1 ... done
Operations to perform:
  Apply all migrations: admin, auth, cms, contenttypes, sessions
Running migrations:
  Applying cms.0002_user_twitter... OK
```
さあこれをユーザーの更新ページと詳細ページに反映させましょう。

```/cms/forms.py
...
class UserUpdateForm(forms.ModelForm):
    class Meta:
        model = UserModel
        fields = ('username', 'first_name', 'last_name', 'email', 'twitter')
...
```

```/cms/templates/cms/user_update.html
...
<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">Twitter</label>
    <div class="control has-icons-left">
        {{ form.twitter }}
        <span class="icon is-small is-left"><i class="fab fa-twitter"></i></span>
    </div>
</div>
...
```

```/cms/templates/cms/user_detail.html
...
{% block content %}
<section class="section">
    <div class="container">
        <div class="level">
            <div class="level-left">
                {% if user.twitter %}
                <div class="level-item">
                    <div class="tags has-addons">
                        <span class="tag"><span class="icon is-small is-left"><i class="fab fa-twitter"></i></span></span>
                        <span class="tag is-dark"><a class="has-text-white" href="https://twitter.com/{{ user.twitter }}" target="_blank">@{{ user.twitter }}</a></span>
                    </div>
                </div>
                {% endif %}
            </div>
        </div>
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ornare magna eros, eu pellentesque tortor vestibulum ut. Maecenas non massa sem. Etiam finibus odio quis feugiat facilisis.</p>
    </div>
</section>
{% endblock %}
```




## ユーザーモデルに対する実装④：ユーザーの削除
さあ、本チュートリアルもこれで最後です。最後にユーザーの削除を実装します。

```/cms/urls.py
...
urlpatterns = [
    ...
    path('user/<int:pk>/delete/', views.UserDelete.as_view(), name='user_delete'),
]
```

```/cms/views.py
...
from django.views.generic.edit import (
    CreateView, UpdateView, DeleteView,
)
...
class UserDelete(OnlyYouMixin, DeleteView):
    model = UserModel
    template_name = 'cms/user_delete.html'
    success_url = reverse_lazy('cms:top')
```

```/cms/templates/cms/user_update.html
...
    </form>
    <hr>
    <a href="{% url 'cms:user_delete' user.pk %}">
        <button class="button is-danger is-outlined">削除</button>
    </a>
</div>
...
```

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

{% block title %}{{ user.username }} | {% endblock %}

{% block content %}
<section class="section">
    <div class="container is-mobile">
        <h1 class="title is-4">ユーザー情報の削除</h1>
        <h2 class="subtitle is-6">Delete your information</h2>
        <hr>
        <p><strong class="has-text-danger">本当にユーザー情報を削除してよろしいですか？</strong></p>
        <br>
        <form method="post" action="">
            {% csrf_token %}
            <div class="field">
                <p class="control">
                    <input type="submit" value="確認して削除" class="button is-danger">
                    <input type="hidden" name="next" value="{{ next }}" >
                </p>
            </div>
        </form>
    </div>
</section>
{% endblock %}
```

最後に管理者画面などをうまく使っていろいろ確認してみてください。


## 最後に
これで終了です。本当にお疲れ様でした！
しかしまだ基本的な部分しか完成していません。ここからが皆さんの色を見せるところです！素晴らしいサービスの完成を運営チーム一同心待ちにしています。

## 参照

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