Djangoで、Class-based generic views + ModelForm を使ってみた

Djangoにて、「フォームで入力すればModelに保存される」という、Modelに紐付いたFormが必要になりました。

調べてみたところ、ModelFormを使えば良さそうでした。
Creating forms from models | Django documentation | Django

 
そこで、Class-based generic view(以下、CBV)でModelFormを使ってみました。

目次

 

環境

  • Windows10
  • Pytnon 3.4.3
  • Django 1.9.1

 

CBV + ModelFormを使うための作業

作業として、

  • データ保存のため、Modelを作成する
  • FormのデータをModelに保存するため、ModelFormを作成する
  • ModelFormをHTMLフォームに使うため、CBVのCreateViewやUpdateViewを作成する
  • HTMLフォームを表示するため、テンプレートを作成する
  • アプリのルーティングを設定する

が必要そうでした。

 
なお、今回はデータの参照・削除・リダイレクトのCBVも試してみようと、

  • django.views.generic.DetailView
  • django.views.generic.ListView
  • django.views.generic.edit.CreateView
  • django.views.generic.edit.UpdateView
  • django.views.generic.edit.DeleteView
  • django.views.generic.base.RedirectView

の計6個のCBVを使います。

 

準備

virtualenv環境の作成から、Djangoアプリの作成までを行います。

d:\Sandbox>mkdir Django_Class_based_view_Sample
d:\Sandbox>cd Django_Class_based_view_Sample
d:\Sandbox\Django_Class_based_view_Sample>virtualenv -p c:\python34\python.exe env
d:\Sandbox\Django_Class_based_view_Sample>env\Scripts\activate
(env) d:\Sandbox\Django_Class_based_view_Sample>pip install django
(env) d:\Sandbox\Django_Class_based_view_Sample>django-admin startproject myproject .
(env) d:\Sandbox\Django_Class_based_view_Sample>python manage.py startapp myapp

 

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

settings.pyに追加します。

INSTALLED_APPS = [
    ...
    'myapp',
]

 

Modelの作成

単純化するため、リレーションのないModelを用意しました。

from django.db import models

class ItemModel(models.Model):
    name = models.CharField('Name', max_length=255, blank=True)
    unit_price = models.DecimalField('UnitPrice', max_digits=10, decimal_places=0)

 

ModelFormの作成

Modelを元にしたModelFormを作ります。

from django import forms
from .models import ItemModel

class ItemForm(forms.ModelForm):
    class Meta:
        model = ItemModel
        fields = '__all__'

なお、Modelのうち一部の項目のみ必要な場合は、fields = ['name', 'unit_price',]のようにします。

 
また、Django1.8よりfieldsまたはexcludeが必須となったため、両方とも存在しない場合は以下のエラーが出ます。
Creating forms from models | Django documentation | Django

django.core.exceptions.ImproperlyConfigured: Creating a ModelForm without either the 'fields' attribute or the 'exclude' attribute is prohibited; form ItemForm needs updating.

 

Viewの作成

今回はすべてCBVで作成します。

CBVは継承関係が分かりづらいため、以下を参考にして継承関係を確認しながら実装を進めます。
Django Class-Based-View Inspector -- Classy CBV

 

更新系のCreateViewやUpdateView

ModelとModelFormを指定します。

class ItemCreate(CreateView):
    model = ItemModel
    form_class = ItemForm

 

参照系のDetailViewやListView、削除系のDeleteView

データソースとして今回はModelを指定します。

class ItemDetail(DetailView):
    model = ItemModel

 

リダイレクトのRedirectView

今回はリダイレクト時のステータスコードを301にしたいのですが、Django1.9よりpermanent = Falseがデフォルトなので、このままではステータスコードが302となります。
Base views | Django documentation | Django

そのため、urlの他に、ステータスコードに関係するpermanentも設定します。

class ItemRedirect(RedirectView):
    url = reverse_lazy('ns:item-list')
    # Django1.9より、permanent=Falseがデフォルトになった
    permanent = True

 

get_success_url()について

CreateView, UpdateView, DeleteViewでは、処理成功時のリダイレクト先のURLをget_success_url()で指定できます。

それに関係していくつか悩んだことがあったので、まとめておきます。

 

新規登録時に採番されたModelのIDを取得する方法について

CreateViewを使って新規登録が成功した後、UpdateViewやDetailViewなどのViewへリダイレクトする場合、新規登録したModelのIDが必要になります。

取得するには、get_success_url()内で self.object.idを使います。
create view - django createview how to get the object that is created - Stack Overflow

 

リダイレクトURLのハードコーディング回避について

URLが変わる可能性があるため、get_success_url()ではURLのハードコーディングを回避したいと考えました。

そのため、以下を参考に、ほとんどのCBVはreverse()、RedirectViewのみreverse_lazy()を使うことにします。

 
なお、urls.pyに記載されたURLのエントリを参照するため、メソッドの第一引数を <project.urlsのnamespace>:<app.urlsのname>という形式で指定します。

また、DetailViewなど、ModelのIDがURLの一部に必要な場合は、メソッドの第二引数をargs=(self.object.id,)とすることで、ModelのIDを渡すことができます。

 

HTMLテンプレートの作成

Viewで指定するHTMLテンプレートを作成します。

なお、テンプレートに関しても悩んだことをまとめておきます。

 

デフォルトのテンプレートファイル名について

template_nameなどの明示的なテンプレート名指定が無い場合、<app_name>/<model_name>_form.htmlなどのデフォルトテンプレート名が使われます。
Form handling with class-based views | Django documentation | Django

ただ、settings.py中のTEMPLATES

# デフォルトでは以下の設定がされているはず
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

 
の場合、path/to/project/<app_name>/templates/<app_name> ディレクトリの中に、以下のテンプレートファイルを置く必要があります。

テンプレートファイル名 テンプレートを使用するView
<model_name>_list.html ListView
<model_name>_detail.html DetailView
<model_name>_form.html CreateView, UpdateViewで共用
<model_name>_confirm_delete.html DeleteView

 
なお、今回のModel名はItemModelというPascalCaseですが、テンプレート名はitemmodel_list.htmlなどと、すべて小文字となります。

 

CSRF対策について

HTMLテンプレートのフォームに {% csrf_token %} が無い場合、Submit時に以下のエラーがブラウザに表示されます。

Forbidden (403)
CSRF verification failed. Request aborted.

 
原因は、CSRF対策の設定不足のためです。

 
そのため、DjangoのHTMLフォームに {% csrf_token %} を忘れずに記述します。

 

Modelのデータを参照する方法について

テンプレートからModelを参照するときのデフォルトオブジェクト名は以下の通りです。

テンプレートで使うオブジェクト名 テンプレートが属するView
object DetailView, DeleteView
object_list ListView
form UpdateView

 
なお、フォームでModelのフィールドを表示するオプションとして、form.as_tableform.as_pform.as_ulなどが用意されています。
Working with forms | Django documentation | Django

 

テンプレートでURLをハードコーディングしない方法について

url <project.urlsのnamespace>:<app.urlsのname>形式で指定します。
Using {% url ??? %} in django templates - Stack Overflow

なお、ModelのIDなどのパラメータが必要な場合には、以下のように、url <project.urlsのnamespace>:<app.urlsのname> <パラメータ>という形式で指定します。

<a href="{% url 'ns:item-update' object.id %}">Go UpdateView</a>

 

ルーティング設定

project.urlsapp.urlsにルーティング設定を記述します。

なお、URLにModelのIDを含む場合、urlpatterns

url(r'^(?P<pk>[0-9]+)/$', views.ItemDetail.as_view(), name='item-detail'),

とすることで、<pk>にModelのIDを引き渡すことができます。
URL dispatcher | Django documentation | Django

 
そのため、プロジェクトのurls.pyは

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^mysite/', include('myapp.urls', namespace='ns')),
]

 
アプリのurls.pyは

urlpatterns = [
    url(r'^$', views.ItemRedirect.as_view(), name='item-redirect'),
    url(r'^create/$', views.ItemCreate.as_view(), name='item-create'),
    url(r'^list/$', views.ItemList.as_view(), name='item-list'),
    url(r'^(?P<pk>[0-9]+)/$', views.ItemDetail.as_view(), name='item-detail'),
    url(r'^(?P<pk>[0-9]+)/update/$', views.ItemUpdate.as_view(), name='item-update'),
    url(r'^(?P<pk>[0-9]+)/delete/$', views.ItemDelete.as_view(), name='item-delete'),
]

となりました。

ちなみに、http://localhost:8000/mysite/にアクセスした際 http://localhost:8000/mysite/listへとリダイレクトする目的で、RedirectViewを使っています。

 

ソースコード

GitHubに上げておきました。
thinkAmi-sandbox/Django_Class_based_view_Sample

 

参考 

公式ドキュメント
その他参考