Django は、Python プログラミング言語で動作するウェブアプリケーションフレームワークです。
この記事は、Django の使い方を説明するものです。
Djangoの解説: The Web framework for perfectionists with deadlines | Django
Django に最初から用意されているビュー(ジェネリックビュー) を使えば、
- オブジェクトのリスト表示
- オブジェクトの作成
- オブジェクトの詳細表示
- オブジェクトの更新
- オブジェクトの削除
のWebアプリが簡単に作成できます。
(オブジェクト=DBのレコード=モデルインスタンス=アクティブレコードオブジェクト という意味です)
Create, Read, Update, Delete をまとめて CRUD と呼んだりしますが、Django では CRUD に対応した汎用的な基底ビューコントローラが用意されているため、それを継承してビューを作ることで、安全で、読みやすいコードを書けます。
それぞれ、以下のビューコントローラに対応しています。
Create … CreateView
Read (リスト表示) … ListView
Read (詳細表示) … DetailView
Update … UpdateView
Delete … DeleteView
django.views.generic
の中にいます。
Django では、これらのさらに基底となる TemplateView
, FormView
もありますが、やっている仕事として「モデルインスタンスを1つ作成/更新」「モデルインスタンス1つを詳しく表示」など、今回の CRUD の基底ビューに含まれる場合は、TemplateView, FormView を使うよりこれらのジェネリックビューを継承したほうがコードを読む際に書き手の意図を理解しやすいため、これらのクラスを使うべきです。
今回は、シンプルなテキストメモを作成・更新するWeb アプリのサンプルを作ってみました。
GitHubにコードを上げてありますので、同時にご参照ください。
リポジトリ
https://github.com/ytyng/django-crud-generic-view-tutorial
核心は、views.py なのでそれを見るとよくわかります。お急ぎの方はこれだけ読んでみてください。
https://github.com/ytyng/django-crud-generic-view-tutorial/blob/master/memo/memo/views.py
環境
Django 1.11, Python 3.6
ディレクトリ構造
memo/
+ manage.py
+ db.sqlite3
+ memo /
+ settings.py (Django設定(自動生成))
+ wsgi.py (WSGIランナー(自動生成))
+ urls.py (URLマップ(自動生成))
+ models.py (Memoモデルの定義)
+ views.py (ビューコントローラ)
+ forms.py (HTMLフォームクラス)
+ migrations (マイグレーションスクリプト(自動生成))
+ templates (HTMLテンプレート)
+ memo
+ base.html
+ memo_confirm_delete.html (削除確認用テンプレート)
+ memo_detail.html (詳細表示用テンプレート)
+ memo_form.html (作成、更新フォーム用テンプレート)
+ base.html (基底テンプレート)
+ memo_list.html (リスト表示用テンプレート)
コード解説
解説はコード中のコメントに書きました
models.py
from django.db import models
class Memo(models.Model):
"""
メモモデル。実質、件名と本文のみ。
"""
subject = models.CharField(
verbose_name='件名',
max_length=100,
default='',
blank=True
)
body = models.TextField(
verbose_name='本文',
default='',
blank=True
)
# created: auto_now_add を指定すると、作成日時を自動保存する
created = models.DateTimeField(
auto_now_add=True
)
# updated: auto_now を指定すると、更新日時を自動保存する
updated = models.DateTimeField(
auto_now=True
)
def __str__(self):
return self.subject
forms.py
from django import forms
from .models import Memo
class MemoForm(forms.ModelForm):
"""
Memo モデルの作成、更新に使われる Django フォーム。
ModelForm を継承して作れば、HTMLで表示したいフィールドを
指定するだけで HTML フォームを作ってくれる。
"""
class Meta:
model = Memo
fields = ['subject', 'body']
views.py
今回の核心です。Django の ListView, DetailView, CreateView, UpdateView, DeleteView をそれぞれ継承し、CRUDのビューコントローラを作成しています。
フォームクラスは、先程の MemoForm を使います。
それぞれのビューで書かなければならない内容は少なく、実務的な処理は Django に任せることで、エンジニアは UI の開発に集中できます。
from django.contrib import messages
from django.urls import reverse_lazy
from django.views.generic import \
ListView, DetailView, CreateView, UpdateView, DeleteView
from .models import Memo
from .forms import MemoForm
class MemoListView(ListView):
"""
メモを一覧表示
テンプレートは、何も指定しないと モデル名_list.html が使われる
ListView は、パジネーションもやってくれる
"""
model = Memo
paginate_by = 10 # 1ページに表示する件数
class MemoDetailView(DetailView):
"""
1つのメモを詳細表示
テンプレートは、何も指定しないと モデル名_detail.html が使われる
"""
model = Memo
class MemoCreateView(CreateView):
"""
メモ 新規作成
完了ページを作成し、success_url で指定して表示してもいいが、
django.contrib.messages の機能で、メッセージを保存して
リストビューなんかに戻した時に表示するのも簡潔で良い。
"""
model = Memo
form_class = MemoForm
success_url = reverse_lazy('memo_list')
def form_valid(self, form):
result = super().form_valid(form)
messages.success(
self.request, '「{}」を作成しました'.format(form.instance))
return result
class MemoUpdateView(UpdateView):
"""
メモ 更新
"""
model = Memo
form_class = MemoForm
success_url = reverse_lazy('memo_list')
def form_valid(self, form):
result = super().form_valid(form)
messages.success(
self.request, '「{}」を更新しました'.format(form.instance))
return result
class MemoDeleteView(DeleteView):
"""
メモ 削除
デフォルトでは、get でリクエストすると確認ページ、
post でリクエストすると削除を実行する、という動作。
実際は、レコードを削除するのではなく有効フラグを消す(いわゆる論理削除)
のケースが多いと思うので、そんな時はdeleteをオーバーライドしてその中で処理を書く。
"""
model = Memo
form_class = MemoForm
success_url = reverse_lazy('memo_list')
def delete(self, request, *args, **kwargs):
result = super().delete(request, *args, **kwargs)
messages.success(
self.request, '「{}」を削除しました'.format(self.object))
return result
urls.py
from django.conf.urls import url
from django.contrib import admin
from . import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$',
views.MemoListView.as_view(),
name='memo_list'),
url(r'^detail/(?P<pk>\d+)/$',
views.MemoDetailView.as_view(),
name='memo_detail'),
url(r'^create/$',
views.MemoCreateView.as_view(),
name='memo_create'),
url(r'^update/(?P<pk>\d+)/$',
views.MemoUpdateView.as_view(),
name='memo_update'),
url(r'^delete/(?P<pk>\d+)/$',
views.MemoDeleteView.as_view(),
name='memo_delete'),
]
テンプレート
Bootstrap4でさっくり作ってあります。
base.html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{# Bootstrap4 で。#}
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css"
integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb"
crossorigin="anonymous">
<title>{% block meta_title %}{% endblock %}</title>
<body>
{% if messages %}
{# Django のメッセージに記録している内容があればここで表示 #}
<div class="container">
<div class="row">
<div class="col-12">
<div class="messages mt-3">
{% for message in messages %}
<div class="alert alert-dismissable alert-{{ message.tags }}" data-alert="alert">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
{{ message }}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
<div class="container">
<div class="row">
<div class="col-12">
<h1 class="mt-5">{% block page_title %}Memo{% endblock %}</h1>
</div>
</div>
</div>
{% block content %}
{% endblock %}
{% block foot_scripts %}
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"
integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"
integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ"
crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>
memo_list.html
シンプルなメモ一覧テンプレート。簡易的なパジネーションつき
{% extends 'memo/base.html' %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<div class="card card-default">
<ul class="list-group list-group-flush">
{% for o in object_list %}
<li class="list-group-item">
<a href="{% url 'memo_detail' o.pk %}">{{ o.subject }}</a>
</li>
{% empty %}
<li class="list-group-item">
メモはありません
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="col-12">
<div class="mt-3 text-center">
{% if page_obj.has_previous %}
<a class="btn btn-secondary" href="/?page={{ page_obj.previous_page_number }}">前へ</a>
{% endif %}
<span>{{ page_obj.number }}/{{ paginator.num_pages }}ページ</span>
{% if page_obj.has_next %}
<a class="btn btn-secondary" href="/?page={{ page_obj.next_page_number }}">次へ</a>
{% endif %}
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 mt-3">
<a href="{% url 'memo_create' %}" class="btn btn-primary">新規作成</a>
</div>
</div>
</div>
{% endblock %}
memo_detail.html
詳細表示。MemoDetailView で使われる。
{% extends 'memo/base.html' %}
{% block meta_title %}{{ object.subject }}{% endblock %}
{% block page_title %}{{ object.subject }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<div class="card card-default">
<div class="card-body">
{{ object.body|linebreaksbr }}
</div>
<div class="card-footer">
<a class="btn btn-primary" href="{% url 'memo_update' object.pk %}">修正</a>
<a class="btn btn-danger" href="{% url 'memo_delete' object.pk %}">削除</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
memo_form.html
新規作成/更新フォーム。 MemoCreateView, MemoUpdateView で使われる。
HTMLフォームは、MemoForm のインスタンスを bootstrap フィルタにかければ自動的にいい感じで作ってくれる。バリデーション〜エラー対応もやってくれる。
{% extends 'memo/base.html' %}
{% load bootstrap %}
{% block meta_title %}{% if object.pk %}{{ object.subject }}{% else %}新規作成{% endif %}{% endblock %}
{% block page_title %}{% if object.pk %}{{ object.subject }}{% else %}新規作成{% endif %}{% endblock %}
{% block content %}
<form method="post">
<div class="container">
<div class="row">
<div class="col-12">
{{ form|bootstrap }}
</div>
</div>
<div class="row">
<div class="col-12">
<button class="btn btn-primary">保存</button>
</div>
</div>
</div>
{% csrf_token %}
</form>
{% endblock %}
memo_confirm_delete.html
削除確認フォーム
{% extends 'memo/base.html' %}
{% load bootstrap %}
{% block meta_title %}削除確認{% endblock %}
{% block page_title %}削除確認{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<p>{{ object }} を削除していいですか?</p>
<form method="post">
<button class="btn btn-danger">削除する</button>
{% csrf_token %}
</form>
</div>
</div>
</div>
{% endblock %}