Djangoでモデルオブジェクトの登録・更新機能は、汎用ビューであるCreateViewやUpdateViewで簡単に実装することができます。
しかし扱うモデルフィールドが多い場合、これらのビューは1つの画面に多数のフォームを描画するために使用性が低くなりがちです。
そこで本エントリでは、このような課題を django-formtools
のフォームウィザードで解決していきます。
github.com
フォームウィザードの完成イメージがこちらです。
ページ切り替えの都度、フォームバリデーションを実行し、中間データはセッションに保存されます。
pipで django-formtools
をインストールします。
pip install django-formtools
執筆時点でバージョンは2.2です。
❯ pip show django-formtools
Name: django-formtools
Version: 2.2
...
モデル
更新対象となるモデルを用意します。
フォームバリデーションおよびファイルアップロードの動きを確認するため、
char1
のみ入力必須、 file
のみFileFieldとしています。
from django.db import models
class MyModel(models.Model):
char1 = models.CharField('項目1', max_length=255)
char2 = models.CharField('項目2', max_length=255, blank=True, null=True)
char3 = models.CharField('項目3', max_length=255, blank=True, null=True)
char4 = models.CharField('項目4', max_length=255, blank=True, null=True)
file = models.FileField('ファイル', blank=True, null=True)
フォーム
フォームウィザードの各ページで表示するフォームをそれぞれModelFormとして作成します。
いずれもモデル MyModel
を扱うため、 Meta.model = MyModel
とし、 Meta.fields
に各ページで表示するフォームフィールドを指定します。
from django import forms
from .models import MyModel
class Form0(forms.ModelForm):
class Meta:
model = MyModel
fields = ['char1', 'char2', 'file']
class Form1(forms.ModelForm):
class Meta:
model = MyModel
fields = ['char3', 'char4']
テンプレート
新規登録・更新ウィザードの両方で利用するテンプレート wizard_form.html
を作成します。
Bootstrapとdjango-crispy-formsを使用した例です。
利用可能なコンテキストの詳細についてはこちらを参照してください。
{% load crispy_forms_tags %}
...
{{ wizard.form.media }}
...
<form method="post" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{{ wizard.management_form }}
{% if wizard.form.forms %}
{{ wizard.form.management_form }}
{% for form in wizard.form.forms %}
{{ form|crispy }}
{% endfor %}
{% else %}
{{ wizard.form|crispy }}
{% endif %}
<div class="row">
<div class="col-lg-12">
{% if wizard.steps.prev %}
<button type="submit" class="btn btn-success pull-left" name="wizard_goto_step" value="{{ wizard.steps.prev }}">
戻る
</button>
{% endif %}
{% if wizard.steps.step1 < wizard.steps.count %}
<button type="submit" class="btn btn-success pull-right">
次へ
</button>
{% else %}
<button type="submit" class="btn btn-success pull-right">
登録
</button>
{% endif %}
</div>
</div>
</form>
ビュー
ビューはWizardViewのサブクラスとして実装します。
中間データの保存方法( WizardView.storage_name
)に応じて、次のクラスが用意されていますので、実際にはこちらを使うと良いです。
以降の説明ではSessionWizardViewを使用していきます。
from formtools.wizard.views import SessionWizardView
class MyWizardView(SessionWizardView):
...
新規作成フォームウィザード
SessionWizardViewを継承し、次の実装を持つ汎用クラスビュー WizardCreateView
を実装します。
import os
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.http import HttpResponseRedirect
from formtools.wizard.views import SessionWizardView
class WizardCreateView(SessionWizardView):
file_storage = FileSystemStorage(
location=os.path.join(settings.MEDIA_ROOT, 'temp')
)
model = None
def done(self, form_list, **kwargs):
data = {}
for form in form_list:
data.update(form.cleaned_data)
self.model.objects.create(**data)
return HttpResponseRedirect(...)
続いて、urlpatternsにpath()を追加します。
as_view()には描画するフォームのクラスをリストで渡します。
使用するテンプレートはtemplate_nameに、
更新対象のモデルは model
に指定します。
from .forms import Form0, Form1
from .models import MyModel
from .views import WizardCreateView
urlpatterns = [
path(
'my-model/new',
WizardCreateView.as_view(
[Form0, Form1],
template_name='wizard_form.html',
model=MyModel,
),
name='create'
),
]
my-model/new
にアクセスすると、 Form0
で定義した3つのフォームが表示されます。
値を入力せずに「次へ」でフォームを送信すると、フォームバリデーションが実行され、必須項目 「項目1」がエラーとなります。
各フォームに適当な値を入力し、再度フォームを送信します。
正常なフォームを送信したため、次のページに遷移し、 Form1
が表示されました。
この時、送信した Form0
の値はセッションに保存されています。
アップロードしたファイルは一時的に WizardCreateView.file_storage
で指定したパスに保存されます。
$ ls media/temp/
test.txt
「戻る」から前のページに戻るとセッションに保存された情報が読み込まれます。
ファイルはフォーム上表示されていませんが、実際には中間ファイルとして保存されているので、そのまま進んでもちゃんと登録されます(関連Issue)。
次のページに再度進み、「項目3」のみ値を入力して「登録」します。
MyModel
から登録したデータが取得できることを確認します。
>>> instance = MyModel.objects.first()
>>> instance.char1
'aaa'
>>> instance.char2
'bbb'
>>> instance.char3
'ccc'
>>> instance.char4
>>> instance.file
<FieldFile: test.txt>
登録が完了すると media/temp
に保存されていた中間ファイルはMEDIA_ROOTに移動します。
ファイルアップロード後に登録を中断した場合には media/temp
にファイルが残ってしまうため、定期的なメンテナンスが必要となる点に注意してください。
$ ls media/
test.txt
$ ls media/temp/
更新ウィザード
新規登録ウィザードと同様に、SessionWizardViewを継承し、次の実装を持つ汎用クラスビュー WizardUpdateView
を実装します。
from django.db.models.fields.files import FieldFile
class WizardUpdateView(SessionWizardView):
file_storage = FileSystemStorage(
location=os.path.join(settings.MEDIA_ROOT, 'temp')
)
model = None
def get_form_instance(self, step):
pk = self.kwargs['pk']
return self.model.objects.get(pk=pk)
def done(self, form_list, **kwargs):
data = {}
for form in form_list:
data.update(form.cleaned_data)
instance = self.model.objects.get(pk=kwargs['pk'])
for attr, value in data.items():
if isinstance(getattr(instance, attr), FieldFile) and not value:
setattr(instance, attr, None)
else:
setattr(instance, attr, value)
instance.save()
return HttpResponseRedirect(...)
URLを追加します。更新対象オブジェクトのPK値をパラメタとして渡すようにします。
urlpatterns = [
...
path(
'my-model/<int:pk>',
WizardUpdateView.as_view(
[Form0, Form1],
template_name='wizard_form.html',
model=MyModel,
),
name='update'
),
]
my-model/<int:pk>
にアクセスして先ほど登録したデータを更新します。
試しに「項目1」を空にして、フォームバリデーションが新規作成時と同様に機能することを確認します。
「項目1」を aaa
から zzz
に変更し、「ファイル」のクリアにチェックを入れて(削除指示)、「次へ」を押下します。
次のページでは、「項目3」に ccc
が登録されています。
「項目3」を ddd
に変更し、「項目4」に eee
を入力して「登録」します。
これで MyModel
の既存データを更新することができました。
>>> instance = MyModel.objects.first()
>>> instance.char1
'zzz'
>>> instance.char2
'bbb'
>>> instance.char3
'ddd'
>>> instance.char4
'eee'
>>> instance.file
<FieldFile: None>
参考