Djangoの汎用クラスビューを用いてシンプルなCRUDアプリを作ってみる

# はじめに
以前、Railsで簡単な投稿・編集・削除ができるアプリを作ったので、Djangoでも再現できるか試してみました。
「読んだ本の感想を投稿する」というテーマで、本のタイトルと短めの感想を投稿できるようになっています。
コードソースを以下に置いておきます。

Rails版：
https://github.com/Sn16799/Bookers.git
Django版：
https://github.com/Sn16799/DjangoBookers.git

# 環境
OS: centos7
Django: 3.0.6
Python: 3.8.3

# アプリの立ち上げ
```
$ python manage.py start project mysite
$ cd mysite
$ python manage.py startapp bookers
```

# ディレクトリ構成
templates以下, forms.pyを手動で付け足しました。

```
mysite/
  bookers/
    templates/
      books/
        index.html
        detail.html
        update.html
    admin.py
    forms.py
    models.py
    urls.py
    views.py
  mysite/
    settings.py
    urls.py

```

# modelの作成・有効化
- **Modelの作成**
 
models.pyを編集します。

```python:bookers/models.py
from django.db import models
from django.urls import reverse

# Create your models here.
class Book(models.Model):
	title = models.CharField('title', max_length=50)
	body = models.CharField('body', max_length=200)

	def get_absolute_url(self):
		return reverse('bookers:detail', kwargs={'pk': self.pk})

```

なるべくシンプルな構成にしたいので、カラムはtitleとbodyのみです。
その下にあるget_absolute_urlは、Modelにデータを保存した後の遷移先を指定するメソッドです。
これを書かずにデータを作成しようとしたら「get_absolute_urlを定義してください！！」というエラーが出ました（メソッドについては、[こちらのサイト](https://it-engineer-lab.com/archives/402#Post)がとても詳しくて分かりやすいです）。

- **プロジェクトにアプリを追加**

settings.pyに下記を追加します。

```python:mysite/settings.py
INSTALLED_APPS = [
    'bookers.apps.BookersConfig', # ここを追加
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
```

Modelの変更をDjangoに知らせるために、migrationを行います。

```
$ python manage.py makemigrations bookers
$ python manage.py migration
```

これでアプリを開発する準備が整いました。

# 管理者サイトを作る
Djangoにはデフォルトで管理者サイトが用意されています。
今後の実装を進めていく上で何かと便利なので、先にadmin側の設定を行っておきましょう。

```python:mysite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('bookers/', include('bookers.urls', namespace='django_bookers')),
    path('admin/', admin.site.urls),
]

```
```python:bookers/admin.py
from django.contrib import admin
from django_bookers.models import Book

# Register your models here.
admin.site.register(Book)
```

以下のコマンドを実行すると管理者アカウントを作成できます。
名前、メールアドレス、パスワードを求められるので、任意の値を入力します。

```
$ python manage.py createsuperuser
```

 http://127.0.0.1:8000/admin/ にアクセスすると、管理者サイトが表示されます。
先ほど作ったユーザ情報でログインすると、アプリごとにレコードの追加や管理を行うことができます。この段階で、いくつかレコードを作っておくことをおすすめします。

# URLの設定
まずはURLを設定します。

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

app_name='bookers'
urlpatterns = [
    # bookers/
    path('', views.CreateView.as_view(), name='index'),
    # bookers/1/
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    # bookers/1/update/
    path('<int:pk>/update/', views.UpdateView.as_view(), name='update'),
    # bookers/1/delete/
    path('<int:pk>/delete', views.delete, name='delete'),
]
```

templateファイルも簡単に書き込んでおきます。後で修正するので、ここでは何の画面か分かる文字が書いてあれば大丈夫です。

```html:templates/books/index.html
<h1>this is INDEX view !!!</h1>
```

```html:templates/books/update.html
<h1>this is UPDATE view !!!</h1>
```

```html:templates/books/detail.html
<h1>this is DETAIL view !!!</h1>
```

# Viewの実装
views.pyを以下のように編集します。

```python:bookers/views.py
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect
from django.views import generic
from .models import Book
from .forms import BookForm

class CreateView(generic.CreateView):
	model = Book
	form_class = BookForm
	template_name = 'books/index.html'

	# Bookモデルのデータを全件取得、book_listに格納
	def get_context_data(self):
		context = super().get_context_data()
		context['book_list'] = Book.objects.all()
		return context

class DetailView(generic.DetailView):
	model = Book
	template_name = 'books/detail.html'
	context_object_name = 'book'

class UpdateView(generic.UpdateView):
	model = Book
	template_name = 'books/update.html'
	form_class = BookForm

def delete(request, pk):
	book = get_object_or_404(Book, id=pk)
	book.delete()
	return redirect('bookers:index')
```

内容を保存したら、各画面が正しく表示されるか確認します。
一覧画面（index.html）:
http://127.0.0.1:8000/bookers
編集画面（update.html）:
http://127.0.0.1:8000/bookers/1/update
詳細画面（detail.html）:
http://127.0.0.1:8000/bookers/1/detail

例）一覧画面
![index.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647951/b42a92a3-6d95-1733-e21c-ad72fa341048.jpeg)


先ほどhtmlファイルに書き込んだ文字が表示されれば、ルーティングの設定は完了です。

# 投稿機能
投稿の一覧表示と新規投稿を同じ画面で行いたいので、CreateViewで機能を作っていきます。一覧表示用にListViewというものも用意してくれているのですが、そちらでは新規投稿の実装が煩雑になりそうだったのでやめました。django.views.genericでは[他にも数多くの便利な汎用ビュー](https://codor.co.jp/django/generic-class-based-view-number)があるようです。

```python:bookers.views.py

class CreateView(generic.CreateView):
        model = Book
        form_class = Bookform
        template_name = 'books/index.html'

	def get_context_data(self):
		context = super().get_context_data()
		context['book_list'] = Book.objects.all()
		return context
```

CreateViewでは、get_context_dataメソッドで任意のデータを渡すことができます。
ここでは、投稿を一覧表示するために、book_listという名前でBookモデルのデータを全て取得しています。
クラスビューで使える引数・メソッドについては、[こちら](https://btj0.com/blog/django/method-attribute/)。

```html:bookers/templates/books/index.html
<h1>Books</h1>

<table>
  <thead>
	<tr>
  	  <th>Title</th>
	  <th>Body</th>
	</tr>
  </thead>
  <tbody>
	{% for book in book_list %}
	<tr>
	  <td>{{ book.title }}</td>
	  <td>{{ book.body }}</td>
	  <td>
		<a href="{% url 'bookers:detail' book.id %}">Detail</a>
		<a href="{% url 'bookers:update' book.id %}">Update</a>
		<a href="{% url 'bookers:delete' book.id %}">Delete</a>
	  </td>
	</tr>
	{% endfor %}
  </tbody>
</table>

<h2>New Book</h2>
<form method="post">
  {{ form.as_p }}
  {% csrf_token %}
  <input type="submit" name="Submit">
</form>

```
ここで http://127.0.0.1:8000/bookers/ にアクセスしてみて、「Books」の見出しと投稿フォームの画面になっていれば成功です。
今の時点では、フォームに入力してSubmitボタンを押すと、詳細画面にリダイレクトされて「this is DETAIL view !!!」（先ほど入力した適当な文言）が出ると思います。ただし、一覧に戻れば保存された投稿がきちんと表示されているのでご安心ください。

![index.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647951/536d71ab-bdc8-afef-461c-bbe62a4c3f61.jpeg)
↑ 画面の装飾を施していないためひどく殺風景な一覧画面

# 詳細ページ
一覧画面で「Detail」のリンクを押した時に遷移する、投稿を1件だけ抜き出して表示できる詳細画面です。今回のアプリではカラムはタイトルと本文のみ、しかも一覧で全部見られるのでこれといった利便性はないですが、練習のために作っておきます。

```python:bookers/views.py
class DetailView(generic.DetailView):
	model = Book
	template_name = 'books/detail.html'
	context_object_name = 'book'
```

context_object_nameでbookを指定したので、テンプレート内でbookという名前でデータを呼び出せるようになりました。なぜかobjectと書いてもカラムの内容を表示できましたが、分かりやすいようbookを明示しておきました。

```html:bookers/templates/books/detail.html
<p>
  <strong>Title:</strong>
  {{ book.title }}
</p>

<p>
  <strong>Body:</strong>
  {{ book.body }}
</p>

<a href="{% url 'bookers:update' book.id %}">Update</a>
 |
<a href="{% url 'bookers:index' %}">Back</a>
```
http://127.0.0.1:8000/bookers/1/detail にアクセスしてみましょう。
![detail.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647951/ee722157-1109-4c5a-f4d3-4985e0d6fd56.jpeg)
↑ 流石に何もなさすぎるので、編集画面（Update）・一覧画面（Back）へのリンクも貼りました


# 編集機能
投稿したデータを後から編集できるようにします。

```python:bookers/views.py
class UpdateView(generic.UpdateView):
	model = Book
	template_name = 'books/update.html'
	form_class = BookForm
```

```html:bookers/templtes/books/update.html
<h1>Updating Book</h1>

<form method="post">
  {{ form.as_p }}
  <button type="submit">Update</button>
</form>

<a href="{% url 'bookers:detail' book.id %}">Detail</a>
 | 
<a href="{% url 'bookers:index' %}">Back</a>
```
（更新処理だからフォームのmethodはpatchでしょ！と自信満々でpatchと書いたら、見事にハマりました。Railsの常識はDjangoの非常識。）

http://127.0.0.1:8000/bookers/1/update にアクセスすると、データの入ったフォームが表示されます。

![update.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647951/ecc4cfbb-51ac-772e-c9c2-d37e52c101a7.jpeg)

実例を提供する意図はなかったのですが、投稿した本のタイトルが間違っていたので訂正したいと思います。
一部を修正し、「Update」ボタンを押します。

![detail_after_update.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647951/7f6f5cc0-24b6-6265-cc92-05dff99c17c0.jpeg)
詳細画面にリダイレクトし、正しいタイトルに直っています！


# 削除機能

```python:bookers/views.py
def delete(request, pk):
	book = get_object_or_404(Book, id=pk)
	book.delete()
	return redirect('bookers:index')
```

一覧画面で削除ボタンを押したらそのまま同じ画面にリダイレクトします。削除確認画面は今回作らなかったので、viewの定義も他の機能とは異なりdefで始まる形（関数ベース汎用ビュー）にしました。

### ※ ビューの種類について
views.pyで作成するビューは、classから始まる**クラスベース汎用ビュー**と、defで始まる**関数ベース汎用ビュー**に大別されます。
今回、作成する画面は3種類なので、create, detail, updateにのみクラスベース汎用ビューで実装しました。
この機能を使うと、少ないコードでCRUD機能やフォームを含む画面を作成できるとのことです。ただし、templateを伴わないviewは作れないため、deleteだけは関数ベース汎用ビューにしています。（詳しくは[こちら](https://qiita.com/dai-takahashi/items/7d0187485cad4418c073)）

# 後記
あちこちエラーにハマりましたが、どうにか最低限の機能がついたアプリを作ることができました。今回は投稿・更新や削除が成功した時のFlashメッセージや、CSSでの装飾までは行わなかったので、後ほど実装したいと思います。

# 参考
#### アプリの作り方全般
[はじめての Django アプリ作成(Djangoドキュメント)](https://docs.djangoproject.com/ja/3.0/intro/tutorial01/)

クラスベースでない削除機能の作り方：
[DjangoBrothers](https://djangobrothers.com/tutorials/memo_app/delete_edit/)

#### クラスベース汎用ビューについて
[Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル](https://qiita.com/dai-takahashi/items/7d0187485cad4418c073)
[Djangoの 汎用クラスビューをまとめて、実装について言及する](https://qiita.com/renjikari/items/af3e8958d2653e6f8d46)

クラスベース汎用ビューの一覧：
[Code for Django](https://codor.co.jp/django/generic-class-based-view-number)

クラスベース汎用ビューで使える引数・メソッド集：
[ガネーシャ様のアンテナショップ](https://btj0.com/blog/django/method-attribute/)

#### その他
success_urlとget_success_url、reverseとreverse_lazyの使い分け：
[ガネーシャ様のアンテナショップ](https://btj0.com/blog/django/success_url-get_success_url-reverse-reverse_lazy/)

get_absolute_url（modelに使ったメソッド）について：
[ITエンジニアラボ](https://it-engineer-lab.com/archives/402)