Python
Django
Heroku

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

この記事について

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

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

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

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

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

参考:Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル

執筆環境

  • Windows 7
  • python 3.6.3
  • Django 2.0.2

この記事で作るもの

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

画面イメージ

画面一覧.png

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

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

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

サンプルコード

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

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

※Windows用(Macでは適切に読み替えてください。)

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

Herokuへのデプロイ

このサンプルはチュートリアルDjango Girls Tutorialと同じ方法でHerokuにアップロードして利用することができます。具体的な作業は以下のページで説明しています。

ただし以下の2点が異なるので適切に修正してください。

  • Procfile にあるプロジェクト名「mysite」は自分で付けたプロジェクト名に変える。サンプルのままなら「project」となる。
Procfile
web: gunicorn project.wsgi
  • チュートリアル内のpsycopg2のバージョンは「2.5.4」を指定しているが、このままではherokuより古すぎて対応できないというエラーが出た(2018/03/12現在)。最新版「2.7.4」に置き換えることで解決した。
requirements.txt
psycopg2==2.7.4

前提知識(参考資料)

チュートリアル一覧

この記事では記事内でアプリケーションの完成を目指すので、Djangoの細かい操作の説明は省きます。その代わりにDjangoの準公式チュートリアルである Django Girls Tutorialの各項目に該当するページを参考記事としてリンクしました。

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

書籍情報

2018/06/18 追記

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

現場で使える 基礎 Django【紙の本】(技術書典4バージョン)

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

基礎から学ぶ-Django-関根-裕紀

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

アプリケーションの作成

作業概要

オブジェクト名について

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

プロジェクト 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モデルについて

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

一般的なMVC Djangoでの呼び方
Model Model
View Template
Controller View

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

手順1.プロジェクト作成

[Django Girls Tutorial 該当ページ]

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

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

mkdir project
cd project
python -m venv env
env\Scripts\activate
pip install django==2.0.8 django-crispy-forms==1.7.2 django-filter==2.0.0 django-pure-pagination==0.3.0
django-admin startproject project .
manage.py startapp app

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

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

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

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

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 
│  ├─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 入力フォームのHTMLをBootstrapに対応させる
django-filter 検索機能を追加する
django-pure-pagination 標準のページング機能を高機能にする

手順2.設定ファイル編集

[Django Girls Tutorial 該当ページ]

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

  • project/settings.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',  #追加
    'pure_pagination', #追加
    'app', #追加
]

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

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

LANGUAGE_CODE = 'ja-JP'

TIME_ZONE = 'Asia/Tokyo'

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

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

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

# django-pure-pagination 設定
PAGINATION_SETTINGS = {
    'PAGE_RANGE_DISPLAYED': 2,
    'MARGIN_PAGES_DISPLAYED': 1,
    'SHOW_FIRST_PAGE_WHEN_INVALID': True,
}

django-pure-paginationの設定についてはこちらの記事が詳しいです。
ganbaruyo.net ページネーション機能の追加 [Django]

手順3.モデル作成

Django Girls Tutorial 該当ページ

app/models.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
    )
    sex = models.IntegerField(
        verbose_name='性別',
        choices=SEX_CHOICES,
        default=1
    )
    memo = models.TextField(
        verbose_name='備考',
        max_length=300,
        blank=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 フォーム自動生成で見出しとして使う

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

公式サイト モデルフィールド一覧

手順4.データベース作成

Django Girls Tutorial 該当ページ

  • Djangoモデル の「データベースにモデル用のテーブルを作る」

モデルの定義後にデータベースの作成を行います。
マイグレーションコマンドを実行してください。
マイグレーションの解説記事

[コマンド]

manage.py makemigrations
manage.py migrate

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

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

公式サイト データベース設定

手順5.管理サイト用の設定

[Django Girls Tutorial 該当ページ]

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

app/admin.py
from django.contrib import admin
from .models import Item

@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
    pass
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を使います。

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

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

手順6.フォーム作成

[Django Girls Tutorial 該当ページ]

image.png

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

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

・app/forms.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}),
                  }

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

手順7.フィルター作成

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

image.png

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

django-filterについては、以下の記事を参照してください。
Django REST framework で django-filter を使う

・app/filters.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',)

手順8.ビュー作成

[Django Girls Tutorial 該当ページ]

ここよりクラスベース汎用ビューを使っていきます。
クラスベース汎用ビューは組み込みのもので相当の数がありますが、今回は以下の5つを使います。FilterViewは追加パッケージのdjango-filterのものです。

参考:クラスベース汎用ビューの一覧

役割 クラス名 要求する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にビューを定義します。
先に説明した5つのクラスベース汎用ビューを継承したクラスを作っていきます。

各ビューでLoginRequiredMixinというクラスを継承していますが、これはログインしていないユーザーがアクセスしたときに、ログイン画面に遷移させる設定を追加するものです。PaginationMixinは django-pure-pagination 用のクラスで検索一覧画面にページング機能を追加します。

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

・app/views.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 pure_pagination.mixins import PaginationMixin
from .models import Item
from .filters import ItemFilter
from .forms import ItemForm


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

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

    # pure_pagination用設定
    paginate_by = 3
    object = Item

    # 検索条件をセッションに保存する 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 該当ページ]

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

・app/urls.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')),
]

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

手順9.テンプレート作成

[Django Girls Tutorial 該当ページ]

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

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

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

テンプレートタグ

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

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

公式サイト:組み込みタグ一覧

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

テンプレートタグ名 説明
date 日付型の表示フォーマットを指定
linebreaksbr 改行を<br>に変更する。
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]

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

AWS / PHP / Python ちょいメモ Django の paginate に GET パラメーター渡し機能を追加

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

・app/templatetags/item_extra.py

app/templatetags/item_extra.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'を'_pagenation.html'で使っています。

サンプルコード

・app/templates/app/_base.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="/admin">管理サイト</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/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/app.js" %}"></script>
</body>

</html>

・app/templates/app/_pagenation.html

ページング機能の部品

app/templates/app/_pagenation.html
{% load item_extras %}
<ul class="pagination">
    {% if page_obj.has_previous %}
    <li class="page-item">
        <a class="page-link" href="?{% url_replace request 'page' page_obj.previous_page_number %}">&laquo;</a>
    </li>
    {% else %}
    <li class="disabled page-item">
        <span class="page-link">&laquo;</span>
    </li>
    {% endif %}
    {% for page in page_obj.pages %}
    {% 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 %}
    {% else %}
    <li class="page-item">
            <a class="page-link"></a>
    </li>
    {% endif %}
    {% endfor %}
    {% if page_obj.has_next %}
    <li class="page-item">
        <a class="page-link" href="?{% url_replace request 'page' page_obj.next_page_number %}">&raquo;</a>
    </li>
    {% else %}
    <li class="disabled page-item">
        <span class="page-link">&raquo;</span>
    </li>
    {% endif %}
</ul>

・app/templates/app/item_card.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

削除画面

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

詳細画面

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

検索一覧画面

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

登録画面・更新画面(共通)

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

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

・app/static/app/css/app.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')
    }
})


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

サンプルアプリの使い方

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

ユーザーの作成

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

image.png

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

image.png

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

image.png

自分でログイン画面を実装するなら「スタッフ権限」は不要です。自作のログイン画面の作り方は以下の記事が参考になります。
Django2 でユーザー認証(ログイン認証)を実装するチュートリアル -2- サインアップとログイン・ログアウト
Djangoでログイン処理を作る
naritoブログ Djangoでログイン画面を自作する

パスワードの変更

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

image.png

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