Django×PinaxでWebサービス入門⑥｜アプリの実装

[前回](https://qiita.com/mtitg/items/a060a5d11659409f5002)はVPSサーバーにて、ローカルで構築したアプリをインターネット公開するところまで進めました。
環境構築周りの面倒なところは全て終了したので、最後にWebアプリケーションを開発していきます。

今回の対象は、全体像で見てみるとこの緑色で囲った部分です。

![02.png](https://qiita-image-store.s3.amazonaws.com/0/113133/9e356c58-1a90-b8bd-f3ec-3483abf686f6.png)

- Djangoアプリケーションの基本構成
- Modelを定義
- Modelを有効化
- Viewを作成
- Templateを作成

## Django アプリケーションの基本構成
Webアプリケーションは通常MVC(Model-View-Controller)という構造で、

- Model: データの定義・ビジネスロジック
- View: ブラウザに返すフロントの定義
- Controller: ModelとViewの制御

といった形でアプリケーション内の機能が大別されます。
DjangoではMVT(Model-View-Template)という分類でそれぞれの機能を呼んでいますが、基本的に呼び方は違いますが振る舞いはMVCと同じなので、MVC:MVTの対応は

- Model: Model
- View: Template
- Controller: View

という風に理解します。

今回TODOアプリを実装するにあたって、まず管理するデータを定義し、どうやってデータをフロントに返すかを定義し、フロントでどう表示するかを定義していきます。
そのため、順番的にはModel→View→Templateという流れで進みます。

## Modelの定義

Modelは`models.py`というファイルにデータの定義を記載したファイルを追加し、その後データベースのテーブル作成・編集用のMigrationファイルを作成し、Migrateコマンドを用いて実際にデータベースに反映させていきます。
`models.py`は下記のようになります。

```python:todo/todo/models.py
from django.db import models
from django.contrib.auth.models import User


class Todo(models.Model):
    title = models.CharField("タイトル", max_length=256)
    content = models.TextField("内容", blank=True)

    def __str__(self):
        return str(self.content)
```

ここでは、`Todo`というエンテティ（データの塊の概念）を定義し、その中に`title(タイトル)`と`content(コンテンツ)`というフィールドを持つようにしています。
`title`の`max_length`は最大文字数を、`content`の`blank=True`は空欄(Nullではない)を許すという意味です。

なお、Djangoは各データのレコードのUUIDを`id`というフィールド名で自動的に生成するので、自らここで定義する必要はありません。

最後の`def __str__(self):`という関数は、`Todo`データを参照した場合にデフォルトでどのフィールドを表示するかを定義しています。

ついでにこの`Todo`というモデルをDjangoの管理画面から確認できるようにしましょう。
このためには`admin.py`というファイルを`models.py`ファイルと同じディレクトリに配置します。

```python:todo/todo/admin.py
from django.contrib import admin
from todo.models import Todo


class TodoAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'content')

admin.site.register(Todo, TodoAdmin)
```

## Modelの有効化

続いて、Djangoの機能である`makemigrations`コマンドを叩き、マイグレーションファイルを作成します。

```terminal:terminal
$ python manage.py makemigrations todo
Migrations for 'todo':
  todo/migrations/0001_initial.py
    - Create model Todo
```
`todo/migrations/0001_initial.py`というファイルが生成されたので中身を見てみます。

```python:todo/migrations/0001_initial.py
from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Todo',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=256)),
                ('content', models.TextField(blank=True)),
            ],
        ),
    ]

```
先程`models.py`で定義した内容をもとに、DBにテーブルを作成するための処理が記載されています。
これを実際にデータベースに反映するために`migrate`コマンドを実行します。

```terminal:terminal
$ python manage.py migrate
Operations to perform:
  Apply all migrations: account, admin, auth, contenttypes, eventlog, sessions, sites, todo
Running migrations:
  Applying todo.0001_initial... OK
```

これで先程のマイグレーションファイルをもとに`Todo`のテーブルが構築されました。これは[Djangoの管理画面](http://127.0.0.1:8000/admin/todo/todo/)から確認できます。(以前作成したスーパーユーザーでログインして見てみてください。)

このように、データベース上の処理は全て`models.py`ファイルとDjangoのコマンドのみで完結しました。これによって、個別のデータベースの使い方等に影響されることなく、アプリケーションの開発に専念することができます。
さらに管理画面でデータの確認・追加・修正・削除ができるため、アプリケーションが正しく動作しているかを確かめるためにデータベースにアクセスするといった必要性もなくなります。

## Viewの作成

続いて、Viewを作成していきます。
ブラウザからのアクセスを受けて、アクセスされたURLに応じてデータベースとやりとりを行い、ブラウザに対してTemplateを返す部分です。

### Generic View

通常、最初は簡単な関数を定義してそれを呼び、データベースに接続して取得したデータをフロントに返却する、という処理をなぞってみると思います。
ただそういった内容は[Django Girls](https://legacy.gitbook.com/book/djangogirlsjapan/workshop_tutorialjp/details) (Djangoの入門用コンテンツがあるサイト。日本語あり)などに説明が豊富にあります。
さらに、この1回から6回までで個人的に書きたかったことは、Pinaxを使った初期構築コストの削減と、VPSにDjangoアプリケーションをデプロイするまでの部分でした。
そのため、今回は引き続きDjangoで簡単にWebアプリケーションを作れるという文脈で、Generic Viewというものを使って作っていきます。

Generic Viewとは、Webアプリケーションを作るうえで頻繁に利用される機能を共通化したクラスの総称で、

- CreateView: データの作成
- DetailView: データの表示
- UpdateView: データの更新
- ListView: データの一覧表示
- DetailView: データの削除

といったいわゆるCRUD的なものから、`TemplateView`という単純にTemplateを返す目的のものや、`FormView `というフォーム(コンタクトフォーム等)を表示するためのものなど17種類が定義されています。
今回は`CreateView`,`UpdateView`,`ListView`,`DetailView`を使っていきます。

### Formの作成

Djangoにはフォームという機構があります。これはユーザーがデータを登録したり編集する時に表示するフォームを、`models.py`で定義したモデルをもとに作成し、バリデーション等の機能を持たせたものです。このフォームをテンプレートの中で呼び出すと、入力編集用の入力欄がまとまって生成されます。

フォームは`fomrs.py`という名前のファイルで作成します。

```python:todo/todo/forms
from django import forms
from todo.models import Todo


class TodoForm(forms.ModelForm):

    class Meta:
        model = Todo # Todoモデルを扱うFormと定義
        fields = ("title", "content") # Formでユーザーが入力可能なフィールド
```

`TodoForm`という名前のフォームを作っています。`Todo`モデルをもとにして、その中で`title`,`content`フィールドを利用するということを定義しています。

### Viewの作成

それではビューを作っていきます。モデルやフォーム同様、`views.py`というファイルを作成します。

```python:todo/todo/views.py
from django.views.generic import CreateView, UpdateView, ListView, DeleteView # 必要なGeneric Viewをインポート
from todo.models import Todo
from todo.forms import TodoForm # 先程作ったフォームをインポート


class TodoCreateView(CreateView): # 新規追加
    model = Todo
    form_class = TodoForm # 新規追加に利用するFormを定義
    success_url = '/todo' # 処理が成功した後のリダイレクト先


class TodoUpdateView(UpdateView): # 更新
    model = Todo
    form_class = TodoForm
    success_url = '/todo'


class TodoListView(ListView): # 一覧
    model = Todo


class TodoDeleteView(DeleteView): # 削除
    model = Todo
    success_url =  '/todo'
````

これだけです。新規追加、更新、一覧、削除のそれぞれにクラスを作成し、どんな働きをするかを明示しています。

ソースが圧倒的に短いですが、これはGeneric Viewによって大半が補完されているためです。
例えば一覧用の`TodoListView`は利用するモデルを定義するだけで、

- 対応するモデルのデータを取得する
- データを返却する先のHTMLファイルを`アプリ名/モデル名_list.html`という名前で探す。(今回は`todo/todo_list.html`)
- 対象のHTMLファイルが見つかったら、そのファイルに対してデータを`object_list`という名前で格納して返す

ということを自動的にやってくれます。
（このあたりのイメージを掴むためには、基礎的なことから自身で書いてみることがやはり大切ですが。。）

## URLパターンの定義

アクセスされるURLと上記のビューを対応させるため、`url.py`を下記のように編集します。

```python:todo/todo/urls.py
from django.conf import settings

from django.conf.urls.static import static
from django.urls import include, path
from django.views.generic import TemplateView
from django.contrib import admin
from todo.views import TodoCreateView, TodoUpdateView, TodoListView, TodoDeleteView # 利用するViewをインポート

urlpatterns = [
    path("", TemplateView.as_view(template_name="homepage.html"), name="home"),
    path("admin/", admin.site.urls),
    path("account/", include("account.urls")),
    path("todo/create", TodoCreateView.as_view(), name='todo-create'), # Todo作成ページ用のURLを追加
    path("todo/update/<pk>/", TodoUpdateView.as_view(), name='todo-update'), # Todo更新ページ用のURLを追加
    path("todo/delete/<pk>/", TodoDeleteView.as_view(), name='todo-delete'), # Todo更新ページ用のURLを追加
    path("todo/", TodoListView.as_view(), name='todo-list') # Todo一覧ページ用のURLを追加
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```
4つの定義を追加しています。

少し細かく見ると、例えば`path("todo/update/<pk>/", TodoUpdateView.as_view(), name='todo-update')`という部分はデータを更新するときのためのURLでして、

- `todo/update/<pk>/`というURLにアクセスがあると（このpkはプライマリーキーの意味です。更新対象のデータを一意に特定する必要があるので）
- `TodoUpdateView`の`as_view()`関数を呼び出す

ということを意味しています。

ちなみに`name='todo-update'`という部分はテンプレートで画面を遷移する時に利用します。

## Templateの作成

最後にテンプレートを作成します。
テンプレートは`todo/todo/templates`というディレクトリを作成してその中に格納していきます。

### 一覧画面

```html:todo/todo/templates/todo_list.html
{% extends "site_base.html" %}

{% block body_base %}

    <main>
        <div class="container">
            <h1 class="h4">Todoリスト</h1>
            <a href="{% url 'todo-create' %}" class="btn btn-primary mb-3">作成する</a>
            <ul class="list-group">
                {% for todo in object_list %}
                    <li class="list-group-item"><a href="{% url 'todo-update' todo.id %}">{{ todo.title }}</a>
                        <a href="{% url 'todo-delete' todo.id %}" class="float-right"><i class="text-dark fa fa-times"></i></a>
                    </li>
                {% endfor %}
            </ul>
        </div>
    </main>

{% endblock %}
```

`TodoListView`から渡されたTodoの一覧は`object_list`という名前で呼び出せます。これをループで回し、リストに表示しています。
またTodo新規作成用のリンク、Todo更新用とTodo削除用のリンクを各リスト内に持っています。
例えば更新だと`{% url 'todo-update' todo.id %}`とありますが、この`todo-update`は先程`urls.py`で定義した`name`, `todo.id`は一意のキーをURLに埋め込むためのものです。(ループ内で`todo`という名前で利用しているため、`todo.id`でidが取得できます。)

### 新規作成・更新画面

新規作成と更新は同じHTMLファイルを利用します。

```html/todo/templates/todo_form.html
{% extends "site_base.html" %}

{% load bootstrap %}

{% block body_base %}

    <main>
        <div class="container">
            <h1 class="h4">Todo作成</h1>
            <form method="post">
                {% csrf_token %}
                    {{ form|bootstrap }}
                <button type="submit">保存する</button>
            </form>
        </div>
    </main>

{% endblock %}
```

`{{ form|bootstrap }}`という部分で、`forms.py`で定義した`TodoForm`を呼び出しています。
上部でBootstrapをロードしているため、`form-control`といったClassが利用できていますが、ここは`{{ form }}`とするだけでも動作します。

### 削除の確認画面

最後に、Todoを削除しようとした時用の確認画面を作ります。

```html:todo/todo/templates/todo_confirm_delete.html
{% extends "site_base.html" %}

{% load bootstrap %}

{% block body_base %}

    <main>
        <div class="container">
            <form method="post">{% csrf_token %}
                <p>"{{ object.title }}" を削除します。よろしいですか"?</p>
                <input type="submit" value="削除する" class="btn btn-primary"/>
            </form>
        </div>
    </main>

{% endblock %}
```

ここでは削除する対象の`Todo`は一つのデータなので、`object_list`ではなく`object`という名前で呼べるようになっています。

## 動かしてみる

ここまでいくと、`http://127.0.0.1:8000/todo/`にアクセスすると一覧画面が、そこからそれぞれに遷移することができます。

![3y1tvTbz1w.gif](https://qiita-image-store.s3.amazonaws.com/0/113133/1b7dcbb7-3043-42ef-58e5-01799f4ce8fb.gif)

こんな感じで動きます。

## 最後に

これで一応Django製Webアプリケーションを作り、VPSにデプロイし、さらに開発を進めていくという土台までが終了しました。
今後Djangoに関する記事を書く時は、ここまでつくったものを拡張する形で進めていければと考えています。