[Python] Djangoチュートリアル - 汎用業務Webアプリを最速で作る


この記事を元に最小の学習コストでWebアプリを作成する方法を記事にしてみました。
こちらもよろしくお願いします。

- **[[Python] プログラム初心者のためのWebアプリ簡単作成法]
(https://qiita.com/okoppe8/items/4cc0f87ea933749f5a49
)**


##この記事について

PythonのWebアプリケーションフレームワーク「Django」についてのチュートリアル記事です。

Djangoには定義したデータモデルを元に一覧画面や入力画面を動的に出力する「クラスベース汎用ビュー（class-based generic views）」という仕組みがあります。

これを活用すると単純なCRUD操作（登録・参照・更新・削除）を行うWebアプリを短時間で作成することができます。さらにDjangoのユーザー管理機能を加えてHeroku等のプラットフォームにデプロイすればインターネット上で運用できます。

Djangoのチュートリアルは良質のものがネットで読めますが、クラスベース汎用ビューの活用まで触れているものは少なく、記事の中で一つの実用的なアプリケーションが完成するくらい要約した内容のものが欲しくて自分で書いてみました。

この記事では少人数での情報共有ツールを想定したサンプルコードを紹介します。
実用的なものをめざしましたので、活用していただけると嬉しいです。

参考：[Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル](https://qiita.com/felyce/items/7d0187485cad4418c073)

**執筆環境**

- python 3.7.0
- Django 2.1.3

##この記事で作るもの

サンプルとして名簿アプリを作ります。

**画面イメージ**

![画面一覧.png](https://qiita-image-store.s3.amazonaws.com/0/217094/732fbc49-bf89-c48f-c8c2-902ebd8c724a.png)


モデルで管理するのは人物情報となります。

このモデルに対し以下の機能をつけます。
**・登録、参照、更新、削除**
**・検索、並べ替え、ページング**
**・ログイン、ユーザ管理（Djangoの基本機能を利用）**


後述しますが、クラスベース汎用ビューはモデルの変更に追従して画面出力項目も変更されるので、これをタスク管理ツールや帳簿管理ツールに容易に変更できます。自由にカスタマイズしてください。


### サンプルコード

Githubにプロジェクトのサンプルコードを置きました。
https://github.com/okoppe8/Django-Simple-CRUD-Sample

アプリの実行には事前にpythonのインストールが必要です。
ダウンロード後、ターミナルでプロジェクトのフォルダに移動し、下のコマンドを順番に実行するとアプリが起動します。ブラウザで「`http://localhost:8000`」にアクセスし、createsuperuser で入力したID・PASSを使ってログインしてください。
    
- Windows

```
python -m venv env
env\Scripts\activate
pip install -r requirements.txt
manage.py migrate
manage.py createsuperuser 
manage.py runserver
```

- MacOS/Linux

```
python3 -m venv env
source env/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py createsuperuser 
```

### インターネットに公開する方法

このサンプルはインターネットに公開してＰＣやスマートフォンから実際に使うことができます。

インターネットへ公開するには何かしらの方法でインターネットに接続されたサーバを用意し設定作業を行う必要があります（これをデプロイといいます）。PaaSというタイプのクラウドサービスなら非常に簡単にデプロイ作業が完了します。

チュートリアル [Django Girls Tutorial](https://tutorial.djangogirls.org/ja/)の現在のバージョンでは「[PythonAnywhere](https://www.pythonanywhere.com)」というPaaSサービスにデプロイする方法を説明しています。

- [Django Girls Tutorial - デプロイ！](https://tutorial.djangogirls.org/ja/deploy/)

しかし PythonAnywhere は日本語の情報に乏しいため運用面で厳しいところがあります。そのため Django Girls Tutorial の以前のバージョンで紹介していた「Heroku」を利用することをおススメします。

- [Django Girls Tutorial - デプロイ！ Heroku版](https://djangogirlsjapan.gitbook.io/workshop_tutorialjp/deploy)

基本的に本文の手続きで大丈夫ですが、以下の２点が異なるので適切に修正してください。

- Procfile にあるプロジェクト名「mysite」は自分で付けたプロジェクト名に変える。サンプルのままなら「project」となる。

```py:Procfile
web: gunicorn project.wsgi
```

- チュートリアル内のpsycopg2のバージョンは「2.5.4」を指定しているが、このままではherokuより古すぎて対応できないというエラーが出た(2018/03/12現在)。最新版「2.7.4」に置き換えることで解決した。

```py:requirements.txt
psycopg2==2.7.4

```

## 前提知識(参考資料)
### チュートリアル一覧
この記事では記事内でアプリケーションの完成を目指すので、Djangoの細かい操作の説明は省きます。その代わりにDjangoの準公式チュートリアルである [Django Girls Tutorial](https://tutorial.djangogirls.org/ja/)より、各項目に該当するページを参考記事としてリンクしました。

それでも説明が至らない点があるとおもうので、適時他の方のチュートリアル記事を参考にしてください。Qiitaだと以下の記事がおすすめです。

- [Python Django入門 (1)](https://qiita.com/kaki_k/items/511611cadac1d0c69c54)

- [Python Django チュートリアルまとめ](https://qiita.com/maisuto/items/17653f344d1f64afa019)

- [Djangoでの開発 ダイジェスト](https://qiita.com/zaburo/items/0e15f6c150caa13ca34c)

- [Django REST Frameworkを使って爆速でAPIを実装する](https://qiita.com/kimihiro_n/items/86e0a9e619720e57ecd8)
※WebAPIの記事だがWebアプリでも構造は大体同じ

### 書籍情報
2018/06/18 追記

この記事の作成時は最新バージョンのDjangoの書籍は皆無でしたが、徐々に書籍も出版されつつあります。以下の２冊を紹介します。

・[現場で使える 基礎 Django【紙の本】（技術書典4バージョン）](https://booth.pm/ja/items/823251)

作者の[akiyoko](https://akiyoko.booth.pm/)さんは実際にDjangoでWeb開発を行っている方で、Djangoの概要から内部動作、現場で使えるベストプラクティス、使えるパッケージを幅広く網羅しており非常にためになりました。
現状では同人誌の扱いであるので売り切れが心配ですが、その際は電子書籍化してほしいですね。

・[基礎から学ぶ-Django-関根-裕紀](https://www.amazon.co.jp/dp/486354247X/)

2018/7/27発売予定で、現時点で未発売です。
こちらの作者の方も実際にDjangoでサービス開発をしています。Codezineで[こちらの連載](https://codezine.jp/article/corner/723)も担当されています。内容も期待できるのではないでしょうか。

## アプリケーションの作成

### 作業概要
 
#### オブジェクト名について

サンプルコードでは汎用アプリとして使うため以下の通り名前を付けました。
流用する場合は具体的な名前に置き換えてください。

|||
|:--|:--|
|プロジェクト|**project**|
|アプリケーション|**app**|
|モデル|**Item**|

#### 作業手順

この記事では以下の手順で作業をすすめます。

|手順|作業内容|作業ファイル| 
|:--:|:--|:--|
|**1**|プロジェクト作成|
|**2**|設定ファイル編集|project/settings.py
|**3**|モデル作成|app/models.py
|**4**|データベース作成|
|**5**|管理サイト設定|app/admin.py、app/apps.py
|**6**|★フォーム作成|app/forms.py
|**7**|★フィルタ作成|app/filters.py
|**8**|ビュー作成|app/urls.py、app/views.py、project/urls.py
|**9**|テンプレート作成|app/templates/* 等

★の項目で作るのはクラスベース汎用ビューで指定する補助クラスです。
指定しなかった場合デフォルトの設定が使われるので、作業を飛ばしても動作に問題はありません。とりあえず完成させて、あとから作成しても大丈夫です。

#### Djnago の MVCパターン

Djangoでは一般的なWebフレームワークと同じくMVCパターンを採用しています。
参考：[MVCモデルについて](https://qiita.com/s_emoto/items/975cc38a3e0de462966a)

ただし各要素の呼び方が異なっており、MTV(Model、View、Template)と呼ばれています。

|一般的なMVC|Djangoでの呼び方|
|:--|:--|
|Model|**Model**|
|View|**Template**|
|Controller|**View**|

まぎらわしいですが、**DjangoのViewとは一般的なMVCのControllerにあたる** ということに注意しましょう。


### 手順１．プロジェクト作成


**[Django Girls Tutorial 該当ページ]**

- [Djangoをインストールしよう](https://tutorial.djangogirls.org/ja/django_installation/)
- [プロジェクトを作成しよう](https://tutorial.djangogirls.org/ja/django_start_project/)
- [Djangoモデル](https://tutorial.djangogirls.org/ja/django_models/)
の Creating an application

まずは基本通り以下のコマンドでプロジェクトとアプリケーションを作成します。

**※本記事のターミナルコマンドについてはWindows環境を前提としています。Macで利用する場合は適切に読み替えてください。**

```
mkdir project
cd project
python -m venv env
env\Scripts\activate
pip install django django-crispy-forms django-filter
django-admin startproject project .
manage.py startapp app
```

**2018/8/17 修正**
~~django-pure-paginationはdjango 2.1に未対応です。
上記の通りバージョン指定してください。~~

**2018/11/27 修正**
django-pure-pagination は今後のメンテナンスが見込めないので利用をやめます。ページネーションには標準の機能を使い、Javascriptのライブラリ「rPage」でレスポンシブ対応を実現します。

実行後、ディレクトリは以下の状態になったはずです。

```
project
│  manage.py
├─app
│  │  __init__.py
│  │  admin.py
│  │  apps.py 
│  │  models.py
│  │  tests.py
│  │  views.py
│  ├─migrations
│  │  __init__.py
└─project
      __init__.py
      settings.py
      urls.py
      wsgi.py
```

この状態に対して、今後の作業で使うフォルダと空ファイルを作成してください。
下の図で★マークのものです。
これが最終形なので以後の作業でファイルは作りません（自動生成分を除く）。

**2018/11/27 修正**
**`responsive-paginate.js`はレスポンシブ対応用のJavascriptのライブラリです。画面幅からあふれたページネーションを自動的に省略します。サンプルコードのファイルはBootstrap4用に修正を入れたものなので、サンプルコードのファイルをそのままコピーして使ってください。**

- 公式はこちら [rPage: Highly responsive pagination for Bootstrap 3](https://github.com/auxiliary/rpage)

```
project
│  manage.py
│  
├─app
│  │  admin.py
│  │  apps.py 
│  │  ★filters.py 
│  │  ★forms.py 
│  │  models.py
│  │  tests.py
│  │  ★urls.py 
│  │  views.py
│  │  __init__.py
│  ├─★static 
│  │  └─★app
│  │      ├─★css 
│  │      │      ★app.css 
│  │      └─★js 
│  │          │  ★app.js 
│  │          └─ ★plugins 
│  │              └─ ★responsive-paginate.js
│  │             
│  ├─migrations
│  │      __init__.py
│  ├─★templates  
│  │  └─★app  
│  │          ★item_card.html 
│  │          ★item_confirm_delete.html 
│  │          ★item_detail.html 
│  │          ★item_filter.html 
│  │          ★item_form.html 
│  │          ★_base.html 
│  │          ★_pagination.html 
│  └─★templatetags 
│          ★item_extras.py 
└─project
        settings.py
        urls.py
        wsgi.py
        __init__.py
```

#### 追加パッケージの説明

pip install で django以外に追加パッケージをインストールしました。  各パッケージの内容は以下の通りです。

|パッケージ名|説明|
|:--|:--|
|[django-crispy-forms](http://django-crispy-forms.readthedocs.io/en/latest/)|入力フォームのHTMLをBootstrapに対応させる|
|[django-filter](https://django-filter.readthedocs.io/en/master/)|検索機能を追加する|

### 手順２．設定ファイル編集

**[Django Girls Tutorial 該当ページ]**

- [プロジェクトを作成しよう](https://tutorial.djangogirls.org/ja/django_start_project/) の「Changing settings」


メインの設定ファイルの編集を行います。
まず、INSTALLED_APPS に追加したパッケージと今回作るアプリケーションを追加します。

- project/settings.py

```py:project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'crispy_forms',  #追加
    'app', #追加
]
```

次はロケールの設定を変更します。
「Internationalization」の項を見つけてLANGUAGE_CODE と TIME_ZONE を変更します。これで管理サイトやエラーメッセージが日本語になります。

```py:project/settings.py
# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/

LANGUAGE_CODE = 'ja-JP'

TIME_ZONE = 'Asia/Tokyo'
```

最後は追加分です。ファイルの最後に以下の内容を追加します。

```py:project/settings.py
# 管理サイトのログイン機能を通常のログイン機能として使う
LOGIN_URL='admin:login'
LOGOUT_REDIRECT_URL='/'

# django-crispy-forms 設定
CRISPY_TEMPLATE_PACK = 'bootstrap4'
```

### 手順３．モデル作成

**Django Girls Tutorial 該当ページ**

 - [Djangoモデル](https://tutorial.djangogirls.org/ja/django_models/)

`app/models.py`を編集してモデルを定義します。
名簿ツールということで、名前・年齢・性別・備考・登録日の定義をしました。

```py:app/models.py

from django.db import models
from django.core import validators


class Item(models.Model):

    SEX_CHOICES = (
        (1, '男性'),
        (2, '女性'),
    )

    name = models.CharField(
        verbose_name='名前',
        max_length=200,
    )
    age = models.IntegerField(
        verbose_name='年齢',
        validators=[validators.MinValueValidator(1)],
        blank=True,
        null=True,
    )
    sex = models.IntegerField(
        verbose_name='性別',
        choices=SEX_CHOICES,
        default=1
    )
    memo = models.TextField(
        verbose_name='備考',
        max_length=300,
        blank=True,
        null=True,
    )
    created_at = models.DateTimeField(
        verbose_name='登録日',
        auto_now_add=True
    )

    # 以下は管理サイト上の表示設定
    def __str__(self):
        return self.name
        
    class Meta:
        verbose_name = 'アイテム'
        verbose_name_plural = 'アイテム'

```
フィールドの種別はなんとなくわかってもらえるとして、ここで使った各オプションの説明は以下の通りです。

|オプション|説明|
|:--|:--|
|auto_now_add|追加時に現在時間を設定|
|blank| 必須入力（デフォルトはTrueなので注意）|
|choices|選択支の自動生成|
|default|デフォルト値|
|max_length|文字長|
|validators|バリデーションの追加|
|verbose_name|フォーム自動生成で見出しとして使う|

そのほかにもたくさんのフィールドとオプションを利用できます。
詳しくは公式サイトを参照してください。

[公式サイト モデルフィールド一覧](https://docs.djangoproject.com/ja/2.1/ref/models/fields/#field-types)

### 手順４．データベース作成

**Django Girls Tutorial 該当ページ**

 - [Djangoモデル](https://tutorial.djangogirls.org/ja/django_models/)
の「データベースにモデル用のテーブルを作る」

モデルの定義後にデータベースの作成を行います。
マイグレーションコマンドを実行してください。
[マイグレーションの解説記事](https://qiita.com/okoppe8/items/c9f8372d5ac9a9679396)


[コマンド]

```
manage.py makemigrations
manage.py migrate
```

デフォルトでsqlite3のファイルがプロジェクト直下に作成されます。

Djangoはsqlite3以外にpostgres・MySQL・Oracleに対応しています。
データベースサーバを使いたい場合は`project/settings.py`の「DATABASES」を変更しましょう。データベースごとの設定方法は公式サイトにあります。

[公式サイト データベース設定](https://docs.djangoproject.com/ja/2.1/ref/databases/)


### 手順５．管理サイト用の設定

**[Django Girls Tutorial 該当ページ]**

- [ログインページを作ろう](https://tutorial.djangogirls.org/ja/django_admin/)

ここでDjangoの管理サイトの設定もしておきます。
`app/admin.py`と`app/apps.py`を編集してください。
admin.pyで管理モデルの登録、app/apps.pyでアプリケーション名の表示を変更しています。

```py:app/admin.py

from django.contrib import admin
from .models import Item

@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
    pass
```
  
```py:app/apps.py

from django.apps import AppConfig

class SampleAppConfig(AppConfig):
    name = 'app'
    verbose_name = 'アプリ'

```

データベースに管理者ユーザーを作成します。以下のコマンドを実行しIDとPASSを設定します。

[コマンド]

```
manage.py createsuperuser
```

完了後開発サーバを起動します。

[コマンド]

```
manage.py runserver
```


ブラウザで「`http://localhost:8000/admin/`」にアクセスしてください。
ログイン画面では先ほど設定したIDとPASSを使います。

設定が間違っていなければ以下のようにメニューが表示され、「アイテム」のリンクより新規アイテムの作成ができるはずです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/217094/f82263de-a1a3-07d4-90d5-2f0c8f8461fe.png" width="400"></img>


ちなみに管理サイトは非常に便利で、今回の記事で作る内容くらいならアプリケーションは作らずとも運用することも可能なくらいです。

### 手順６．フォーム作成

**[Django Girls Tutorial 該当ページ]**

- [フォームを作ろう](https://tutorial.djangogirls.org/ja/django_forms/)

- [こちらも参考に](https://qiita.com/okoppe8/items/b8b110803a89b09b00bd)。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/1cfe7128-af04-00c2-a132-060cfc7d8eb8.png)


入力フォームを生成する設定クラスを定義します。ここで設定した内容は、Itemモデルのもともとの設定に加えられ、そのまま上の画像の赤枠部分に反映されます。

ここではHTMLタグに属性の追加、性別の選択をセレクトからラジオボタンに変更しています。

・app/forms.py

```py:app/forms.py  

from django import forms
from .models import Item


class ItemForm(forms.ModelForm):

    class Meta:
        model = Item
        fields = ('name','age','sex','memo')
        widgets = {
                    'name': forms.TextInput(attrs={'placeholder':'記入例：山田　太郎'}),
                    'age': forms.NumberInput(attrs={'min':1}),
                    'sex': forms.RadioSelect(),
                    'memo': forms.Textarea(attrs={'rows':4}),
                  }

```
  
今回は登録画面と更新画面で同じフォームを使いますが、異なるフォームを使いたいときはここで別々に定義し、ビュー定義上でそれぞれのフォームを参照します。
  
  
                  
                 

### 手順７．フィルター作成



検索フォームを生成する設定クラスを定義します。
このクラスは追加パッケージ「django-filter」の設定となります。ここで定義した内容は検索一覧画面の検索フォームに反映されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/8e67ffc6-645d-0ae6-56ee-19170ed1ccd7.png)


django-filterでは検索条件のデフォルトは完全一致です。このままでは使いづらいので以下のサンプルでは部分一致に変更しています。フィルターの時と違い、モデルのフィールドと同名のフィールドを定義して設定を上書きする方法を採っています。
order_byというフィールドを定義すると、検索に加えて並び順の指定ができます。

django-filterについては、以下の記事を参照してください。
  [Django REST framework で django-filter を使う](https://qiita.com/okoppe8/items/77f7f91f6878e3f324cc#%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%81%AE%E5%AE%9A%E7%BE%A9)

・app/filters.py

```py:app/filters.py
from django_filters import filters
from django_filters import FilterSet
from .models import Item


class MyOrderingFilter(filters.OrderingFilter):
    descending_fmt = '%s （降順）'


class ItemFilter(FilterSet):

    name = filters.CharFilter(label='氏名', lookup_expr='contains')
    memo = filters.CharFilter(label='備考', lookup_expr='contains')

    order_by = MyOrderingFilter(
        # tuple-mapping retains order
        fields=(
            ('name', 'name'),
            ('age', 'age'),
        ),
        field_labels={
            'name': '氏名',
            'age': '年齢',
        },
        label='並び順'
    )

    class Meta:

        model = Item
        fields = ('name', 'sex', 'memo',)

```

### 手順８．ビュー作成

**[Django Girls Tutorial 該当ページ]**

- [Django ビューってなに？](https://tutorial.djangogirls.org/ja/django_views/)


ここよりクラスベース汎用ビューを使っていきます。
クラスベース汎用ビューは組み込みのもので相当の数がありますが、今回は以下の５つを使います。FilterViewは追加パッケージのdjango-filterのものです。
  
参考：[クラスベース汎用ビューの一覧](https://docs.djangoproject.com/ja/2.1/ref/class-based-views/)

|役割|クラス名|要求するURL|HTTPメソッド|
|:--|:--|:--|:--|
|検索一覧画面|FilterView|/page/|GET|
|詳細画面|DetailView|/page/id|GET|
|登録画面|CreateView|/page/|GET,POST|
|更新画面|UpdateView|/page/id|GET,POST|
|削除画面|DeleteView|/page/id|GET,POST|
 
#### app/views.pyの作成

views.pyにビューを定義します。
先に説明した５つのクラスベース汎用ビューを継承したクラスを作っていきます。

各ビューで`LoginRequiredMixin`というクラスを継承していますが、これはログインしていないユーザーがアクセスしたときに、ログイン画面に遷移させる設定を追加するものです。

FilterViewのところにロジックがありますが、詳細ページから一覧ページに戻るときに、検索条件を維持して再表示させるための処理となります。

・app/views.py

```py:app/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django_filters.views import FilterView

from .models import Item
from .filters import ItemFilter
from .forms import ItemForm


# Create your views here.
# 検索一覧画面
class ItemFilterView(LoginRequiredMixin, FilterView):
    model = Item
    filterset_class = ItemFilter
    # デフォルトの並び順を新しい順とする
    queryset = Item.objects.all().order_by('-created_at')

    # クエリ未指定の時に全件検索を行うために以下のオプションを指定（django-filter2.0以降）
    strict = False

    # 1ページあたりの表示件数
    paginate_by = 10

    # 検索条件をセッションに保存する or 呼び出す
    def get(self, request, **kwargs):
        if request.GET:
            request.session['query'] = request.GET
        else:
            request.GET = request.GET.copy()
            if 'query' in request.session.keys():
                for key in request.session['query'].keys():
                    request.GET[key] = request.session['query'][key]

        return super().get(request, **kwargs)


# 詳細画面
class ItemDetailView(LoginRequiredMixin, DetailView):
    model = Item


# 登録画面
class ItemCreateView(LoginRequiredMixin, CreateView):
    model = Item
    form_class = ItemForm
    success_url = reverse_lazy('index')


# 更新画面
class ItemUpdateView(LoginRequiredMixin, UpdateView):
    model = Item
    form_class = ItemForm
    success_url = reverse_lazy('index')


# 削除画面
class ItemDeleteView(LoginRequiredMixin, DeleteView):
    model = Item
    success_url = reverse_lazy('index')
```

#### app/urls.pyの作成


**[Django Girls Tutorial 該当ページ]**

- [Django urls ってなに？](https://tutorial.djangogirls.org/ja/django_urls/)

Viewの定義後にクラスベース汎用ビューへのルーティングを設定します。

・app/urls.py

```py:app/urls.py
from django.urls import path
from .views import ItemFilterView, ItemDetailView, ItemCreateView, ItemUpdateView, ItemDeleteView


urlpatterns = [
    # 一覧画面
    path('',  ItemFilterView.as_view(), name='index'),
    # 詳細画面
    path('detail/<int:pk>/', ItemDetailView.as_view(), name='detail'),
    # 登録画面
    path('create/', ItemCreateView.as_view(), name='create'),
    # 更新画面
    path('update/<int:pk>/', ItemUpdateView.as_view(), name='update'),
    # 削除画面
    path('delete/<int:pk>/', ItemDeleteView.as_view(), name='delete'),
]

```

#### project/urls.py の編集

ルーティングの設定をします。管理機能(admin)はそのままにし、サイトの直下へのアクセスをアプリケーションにルーティングします。

```project/urls.py

from django.contrib import admin
from django.urls import path, include 

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('app.urls')),
]
```

これでサイトへのアクセスが各クラスベース汎用ビューにルーティングされるようになりました。

### 手順９．テンプレート作成

**[Django Girls Tutorial 該当ページ]**

- [テンプレートに表示しよう](https://tutorial.djangogirls.org/ja/django_templates/)
- [CSSでかわいくしよう](https://tutorial.djangogirls.org/ja/css/)
- [テンプレートを拡張しよう](https://tutorial.djangogirls.org/ja/template_extending/)
- [アプリケーションを拡張しよう](https://tutorial.djangogirls.org/ja/extend_your_application/)

#### テンプレート作成の基本方針

Djangoの標準テンプレートは、拡張子「.html」のファイルの中に「{{}}」もしくは「{%%}」を使って処理を埋め込む形式です。テンプレートを部品化し、ひとつのページの中で複数のテンプレートを呼び出すことができます。

一般的な基本方針としては、htmlファイルの内容をbodyタグの外側と内側にわけ、外側（HEADタグと共通で読み込むJavascriptライブラリの記述）を「_base.html」という名前の共通テンプレートとし、内側部分を個別に作っていくことになります。

#### テンプレートタグ

テンプレートでモデルの値を利用する際、bool値を「未：済」という表示に変換したり、ログインユーザーの名前を表示したりさまざまな機能を提供するのがテンプレートタグです。

組み込みのテンプレートタグでもかなりの数があります。また自作することも可能です。
表示上の問題はテンプレートタグの工夫で解決することが多いので、とりあえず公式サイトの説明を一読しましょう。

公式サイト：[組み込みタグ一覧](https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/)


今回のサンプルでは以下のテンプレートタグを使用しています。

|テンプレートタグ名|説明
|:--|:--|
|[date](https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#std:templatefilter-date)|日付型の表示フォーマットを指定
|[linebreaksbr](https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaksbr)|改行を`<br>`に変更する。
|[get_FOO_display](https://docs.djangoproject.com/ja/2.1/ref/models/instances/#django.db.models.Model.get_FOO_display)|choicesの表示値を使う（タグはでない）|



#### クラスベース汎用ビューのデフォルトテンプレート名

クラスベース汎用ビューで`template`を定義しない場合、Modelで指定したモデル名にもとづいたファイル名のテンプレートが呼び出されます。その命名規則に従ってテンプレートをapp/templates/app/以下に配置します。

|機能|クラス名|テンプレート名|
|:--|:--|:--|
|検索一覧画面|FilterView|item_filter.html
|詳細画面|DetailView|item_detail.html|
|登録画面|CreateView|item_form.html|
|更新画面|UpdateView|item_form.html|
|削除画面|DeleteView|item_comfirm_delete.html

※[item]はModelフィールドで指定したクラス名。違うモデル名のときは置き換える。

#### ページング機能について

ページング機能についてはこちらのサイトを参考にしました。
[ganbaruyo.net ページネーション機能の追加 [Django]](https://ganbaruyo.net/details/add-pagination-to-django-class-base-view/)


ただしこのままでは事前に検索を行っていた場合、ページ移動時に検索パラメータを引き継いでもらえないという問題があります。
これについてもすでに解決した方がいました。こちらのサイトの内容をそのまま使わせてもらっています。

[AWS / PHP / Python ちょいメモ Django の paginate に GET パラメーター渡し機能を追加](http://hideharaaws.hatenablog.com/entry/2015/01/16/002813)

この内容にもとづき作成しているのが以下のコードです。

・app/templatetags/item_extras.py

```py:app/templatetags/item_extras.py
from django import template

register = template.Library()

@register.simple_tag
def url_replace(request, field, value):
    dict_ = request.GET.copy()
    dict_[field] = value
    return dict_.urlencode()
```

ここで定義した'url_replace'を'_pagination.html'で使っています。

#### サンプルコード

・app/templates/app/_base.html

共通テンプレート

```html:app/templates/app/_base.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">

<head>
    <!-- Required meta tags always come first -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <title>アプリケーション名</title>
    <!-- Bootstrap CSS -->
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
        crossorigin="anonymous">
    <link href="{% static "app/css/app.css" %}" rel="stylesheet">
</head>

<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <a class="navbar-brand" href="#">アプリケーション名</a>
        <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#Navber" aria-controls="Navber" aria-expanded="false"
            aria-label="ナビゲーションの切替">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="Navber">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'admin:index'%}">管理サイト</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'admin:logout'%}">ログアウト</a>
                </li>
            </ul>
        </div>
        <!-- /.navbar-collapse -->
    </nav>
    {% block content %} 
    {% endblock %}

    <!-- jQuery first, then Tether, then Bootstrap JS. -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
        crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
    <script src="{% static "app/js/plugins/responsive-paginate.js" %}"></script>
    <script src="{% static "app/js/app.js" %}"></script>
</body>

</html>

```


・app/templates/app/_pagination.html

ページング機能の部品

```html:app/templates/app/_pagination.html

{% load item_extras %}
<ul class="pagination">
    {% if page_obj.has_previous %}
        <li class="page-item pagination-prev">
            <a class="page-link" href="?{% url_replace request 'page' page_obj.previous_page_number %}">&laquo;</a>
        </li>
    {% else %}
        <li class="disabled page-item pagination-next">
            <span class="page-link">&laquo;</span>
        </li>
    {% endif %}
    {% for page in page_obj.paginator.page_range %}
        {% if page %}
            {% ifequal page page_obj.number %}
                <li class="active page-item">
                    <span class="page-link">{{ page }}
                        <span class="page-link sr-only">(current)</span>
                    </span>
                </li>
            {% else %}
                <li class="page-item">
                    <a class="page-link" href="?{% url_replace request 'page' page %}">{{ page }}</a>
                </li>
            {% endifequal %}
        {% endif %}
    {% endfor %}
    {% if page_obj.has_next %}
        <li class="page-item pagination-next">
            <a class="page-link" href="?{% url_replace request 'page' page_obj.next_page_number %}">&raquo;</a>
        </li>
    {% else %}
        <li class="disabled page-item pagination-next">
            <span class="page-link ">&raquo;</span>
        </li>
    {% endif %}
</ul>
```

・app/templates/app/item_card.html

データの詳細を表示。詳細画面と削除画面で共通利用

```html:app/templates/app/item_card.html
<div class="row">
    <div class="col-3">
        <p>名前</p>
    </div>
    <div class="col-9">
        <p>{{ item.name }}</p>
    </div>
</div>
<div class="row">
    <div class="col-3">
        <p>年齢</p>
    </div>
    <div class="col-9">
        <p>{{ item.age }}</p>
    </div>
</div>
<div class="row">
    <div class="col-3">
        <p>性別</p>
    </div>
    <div class="col-9">
        <p>{{ item.get_sex_display }}</p>
    </div>
</div>
<div class="row">
    <div class="col-3">
        <p>備考</p>
    </div>
    <div class="col-9">
        <p>{{ item.memo|linebreaksbr }}</p>
    </div>
</div>
<div class="row">
    <div class="col-3">
        <p>登録日</p>
    </div>
    <div class="col-9">
        <p>{{ item.created_at|date:"Y/m/d G:i:s" }}</p>
    </div>
</div>
```

・app/templates/app/item_confirm_delete.html

削除画面

```html:app/templates/app/item_confirm_delete.html
{% extends "./_base.html" %}
<!--  -->
{% block content %}
<div class="container">
    <h2 class="text-center">データ削除</h2>
    <p>このデータを削除します。よろしいですか？</p>

    <form action="" method="post">
        {% csrf_token %}
        <div class="row">
            <div class="col-12">
                <div class="float-right">
                    <a class="btn btn-outline-secondary" href="{% url 'index' %}">戻る</a>
                    <input type="submit" class="btn btn-outline-secondary" value="削除" />
                </div>
            </div>
        </div>
        {% include "./item_card.html" %}
        <div class="row">
            <div class="col-12">
                <div class="float-right">
                    <a class="btn btn-outline-secondary" href="{% url 'index' %}">戻る</a>
                    <input type="submit" class="btn btn-outline-secondary" value="削除" />
                </div>
            </div>
        </div>
    </form>
</div>
{% endblock %}
```

・app/templates/app/item_detail.html

詳細画面

```html:app/templates/app/item_detail.html
{% extends "./_base.html" %}
{% block content %}
<div class="container">
	<h2 class="text-center">詳細表示</h2>
	<div class="row">
		<div class="col-12">
			<a class="btn btn-outline-secondary float-right" href="{% url 'index' %}">戻る</a>
		</div>
	</div>
	<!--  -->
	{% include "./item_card.html" %}
	<div class="row">
		<div class="col-12">
			<a class="btn btn-outline-secondary float-right" href="{% url 'index' %}">戻る</a>
		</div>
	</div>
</div>
{% endblock %}
```

・app/templates/app/item_filter.html

検索一覧画面

```html:app/templates/app/item_filter.html
{% extends "./_base.html" %}
{% block content %} 
{% load crispy_forms_tags %}
<div class="container">
	<div id="myModal" class="modal fade" tabindex="-1" role="dialog">
		<div class="modal-dialog" role="document">
			<div class="modal-content">
				<div class="modal-header">
					<h5 class="modal-title">検索条件</h5>
					<button type="button" class="close" data-dismiss="modal" aria-label="閉じる">
						<span aria-hidden="true">&times;</span>
					</button>
				</div>
				<form id="filter" method="get">
					<div class="modal-body">
						{{filter.form|crispy}}
					</div>
				</form>
				<div class="modal-footer">
					<a class="btn btn-outline-secondary" data-dismiss="modal">戻る</a>
					<button type="submit" class="btn btn-outline-secondary" form="filter">検索</button>
				</div>
			</div>
		</div>
	</div>
	<div class="row">
		<div class="col-12">
			<a class="btn btn-secondary filtered" style="visibility:hidden" href="/?page=1">検索を解除</a>
			<div class="float-right">
				<a class="btn btn-outline-secondary" href="{% url 'create' %}">新規</a>
				<a class="btn btn-outline-secondary" data-toggle="modal" data-target="#myModal" href="#">検索</a>
			</div>
		</div>
	</div>

	<div class="row" >
		<div class="col-12">
			{% include "./_pagination.html" %}
		</div>
	</div>

	<div class="row">
		<div class="col-12">
			<ul class="list-group">
				{% for item in item_list %}
				<li class="list-group-item">
					<div class="row">
						<div class="col-3">
							<p>名前</p>
						</div>
						<div class="col-9">
							<p>{{ item.name }}</p>
						</div>
					</div>
					<div class="row">
						<div class="col-3">
							<p>登録日</p>
						</div>
						<div class="col-9">
							<p>{{item.created_at|date:"Y/m/d G:i:s"}}</p>
						</div>
					</div>
					<div class="row">
						<div class="col-12">
							<div class="float-right">
								<a class="btn btn-outline-secondary " href="{% url 'detail' item.pk %}">詳細</a>
								<a class="btn btn-outline-secondary " href="{% url 'update' item.pk %}">編集</a>
								<a class="btn btn-outline-secondary " href="{% url 'delete' item.pk %}">削除</a>
							</div>
						</div>
					</div>
				</li>
				{% empty %}
				<li class="list-group-item">
					対象のデータがありません
				</li>
				{% endfor %}
			</ul>
		</div>
	</div>
	<div class="row" >
		<div class="col-12">
			<div class="float-right">
				<a class="btn btn-outline-secondary" href="{% url 'create' %}">新規</a>
				<a class="btn btn-outline-secondary" data-toggle="modal" data-target="#myModal" href="#">検索</a>
			</div>
		</div>
	</div>
</div>
{% endblock %}
```

・app/templates/app/item_form.html

登録画面・更新画面（共通）

```html:app/templates/app/item_form.html
{% extends "./_base.html" %}
{% load crispy_forms_tags %}
{% block content %}
{{ form.certifications.errors }}
<div class="container">
    <div class="row">
        <div class="col-12">
            <h2 class="text-center">データ入力</h2>
        </div>
    </div>
    <div class="row">
        <div class="col-12">
            <div class="float-right">
                <a class="btn btn-outline-secondary" href="{% url 'index' %}">戻る</a>
                <a class="btn btn-outline-secondary save" href="#">保存</a>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-12">
            <form method="post" id="myform">
                {%crispy form%}
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-12">
            <div class="float-right">
                <a class="btn btn-outline-secondary" href="{% url 'index' %}">戻る</a>
                <a class="btn btn-outline-secondary save" href="#">保存</a>
            </div>
        </div>
    </div>
</div>
{% endblock %}
```

共通css/js

・app/static/app/css/app.css

```css:app/static/app/css/app.css
.row{
    margin-top:3px;
    margin-bottom:3px;
}
```

・app/static/app/css/app.js

**rPageについて**
ページネーション（特定ページへのリンク）が多いときに、画面幅からあふれる分を自動的に省略するプラグインです。


```js:app/static/app/js/app.js
// 入力フォームでリターンキー押下時に送信させない
$('#myform').on('sumbit', function (e) {
    e.preventDefault();
})

// 連続送信防止
$('.save').on('click', function (e) {
    $('.save').addClass('disabled');
    $('#myform').submit();
})

// [検索を解除] の表示制御
conditions = $('#filter').serializeArray();
$.each(conditions, function(){
    if(this.value){
        $('.filtered').css('visibility','visible')
    }
})

// ページネーションのレスポンシブ対応
// https://auxiliary.github.io/rpage/
$(".pagination").rPage();

```


以上でアプリケーションの作成は完了です。

## サンプルアプリの使い方

このアプリケーションの運用方法について簡単に説明します。

### ユーザーの作成

まずcreatesuperuserで作成したユーザーでログインします。アプリケーションが表示されたら、上部メニューの「管理サイト」をクリックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/7ef8a031-6384-7d75-a716-cead03c6f074.png)

その後、管理サイトの「ユーザーの追加」をクリックし、利用する人数分のユーザーを作成しましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/f1da5179-18ca-34eb-a010-35de15b1e277.png)

**ユーザー情報の設定では、必ず「スタッフ権限」にチェックを入れてください。サンプルアプリでは管理サイトアプリのログイン画面を通常のログイン機能として使うためこの設定が必要です。**


![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/5f2cb306-63d3-86ec-c10b-3b29453cd650.png)

自分でログイン画面を実装するなら「スタッフ権限」は不要です。自作のログイン画面の作り方は以下の記事が参考になります。
[Django2 でユーザー認証（ログイン認証）を実装するチュートリアル -2- サインアップとログイン・ログアウト](https://it-engineer-lab.com/archives/544)
[Djangoでログイン処理を作る](https://qiita.com/maueki/items/d28fd2a170d42e745376)
[naritoブログ Djangoでログイン画面を自作する](https://torina.top/detail/222/)

### パスワードの変更

ユーザー作成後、各ユーザーでログインしてもらいます。
スタッフユーザーは何の権限も無い場合でもパスワードの変更だけは行えます。以下のリンクより変更できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/42206384-62c8-5b15-428f-7918f0335151.png)

アプリケーションのトップ画面へは「サイトを表示」のリンクから移動できます。

## 更新内容（2018/11/26）

- Django 2.1.3 に対応
- Django Girls Tutorial へのリンクを修正。デプロイについては旧版のリンクも残す
- `django-pure-pagination`のメンテナンスが期待できないので、ページネーションは通常の機能を使い `rPage` を使ってレスポンシブ対応を行う。