Djangoでフォームの確認画面(プレビュー)を出す方法を調べてみたところ、django.contrib.formtools.FormPreview
がありました。
ただ、Django1.8よりdjango.contrib.formtools
がライブラリdjango-formtools
として切り出されていました。
- django.contrib.formtools | Django documentation | Django
- django/django-formtools: A set of high-level abstractions for Django forms
そこで、以下を参考に、Python3.5 + Django1.9 + django-formtoolsを試してみることにしました。
環境
今回はManyToManyFieldをModelに持つDjangoアプリの確認画面を、以下の環境で作りました。
準備
d:\Sandbox\django_form_preview_sample>virtualenv -p c:\python35-32\python.exe env d:\Sandbox\django_form_preview_sample>env\Scripts\activate (env) d:\Sandbox\django_form_preview_sample>pip install django (env) d:\Sandbox\django_form_preview_sample>pip install django-formtools (env) d:\Sandbox\django_form_preview_sample>django-admin startproject myproject . (env) d:\Sandbox\django_form_preview_sample>python manage.py startapp myapp
Model
ManyToManyFieldを持つModelを用意します。
class Category(models.Model): name = models.CharField('category', max_length=255) # 表示した時に # Category object # のようになるのを防ぐため、__str__を定義 def __str__(self): return self.name class Article(models.Model): title = models.CharField('title', max_length=255) categories = models.ManyToManyField(Category) content = models.TextField('content')
また、Categoryの初期データ用に、fixtureのpath/to/project/myapp/fixtures/initial_data.json
を用意します。
View
確認画面で使うビューは、from formtools.preview.FormPreview
を継承します。
FormPreviewはgeneric class-based viewではないため、FormPreviewのdoneメソッドを使って、保存処理とリダイレクト処理を自分で作成します。
今回、ManyToManyFieldのModelを扱うことから、
- ModelFormの
save_m2m()
メソッドを使う方法 - ModelFormを使わない方法
の2種類の保存方法を試してみます。
ModelFormのsave_m2m()
メソッドを使う方法
簡潔ですが、引数のcleaned_data
を使っていないため、無駄がありそうです。
class ArticlePreviewUsingRequest(FormPreview): def done(self, request, cleaned_data): f = ArticleForm(request.POST) article = f.save(commit=False) article.save() f.save_m2m() url = reverse('ns:article-detail', args=(article.id,)) return HttpResponseRedirect(url)
ModelFormを使わない方法
cleaned_data
を使う方法を調べてみたところ、以下のstackoverflowがあったため、それを参考に実装してみます。
python - Django Forms - Many to Many relationships - Stack Overflow
class ArticlePreviewUsingCleanedData(FormPreview): def done(self, request, cleaned_data): article = Article.objects.create( title = cleaned_data['title'], content = cleaned_data['content'], ) article.categories.add(*cleaned_data['categories']) url = reverse('ns:article-detail', args=(article.id,)) return HttpResponseRedirect(url)
save_m2m()より多少増えましたが、これでも十分簡潔に見えました。
urls.py
今回試した、ModelFormあり/なしの両方のパターンを試せるよう、
urlpatterns = [ url(r'^request/$', views.ArticlePreviewUsingRequest(forms.ArticleForm), name='article-request'), url(r'^cleaned_data/$', views.ArticlePreviewUsingCleanedData(forms.ArticleForm), name='article-cleaned_data'), url(r'^(?P<pk>[0-9]+)/$', views.ArticleDetail.as_view(), name='article-detail'), ]
としました。
Template
確認画面について
FormPreviewで使われるデフォルトテンプレートは
- formtools/form.html
- formtools/preview.html
の2つになります。
ひな型がそれぞれ
- django-formtools/form.html at master · django/django-formtools
- django-formtools/preview.html at master · django/django-formtools
に用意されています。
このテンプレートを元に、formtools/preview.html
へ
<table> {% for field in form %} <tr> <th>{{ field.label }}:</th> <td>{{ field.data }}</td> </tr> {% endfor %} </table>
と書いてみたところ、Modelの各項目を持った確認画面ができました。
ただ、ManyToManyFieldであるcategoriesの値が['2', '3']
と、optionタグのvalue属性値となっていました。
ManyToManyFieldをoptionタグのテキスト内容で表示する方法について
調べてみたところ、自作テンプレートタグを使えば良さそうでした。
DjangoのFormPreviewを使って表示した確認ページでchoicesの表示 - 雑記
自作テンプレートタグを作成するにあたり、まずは
path/to/project/myapp/templatetags/field_extras.py
@register.filter def all_attr(bound_form): # すべてのフィールドではattrが共通と思われるので、 # 最初のでだけ確認すればいいはず for f in bound_form: return dir(f)
path/to/project/myapp/templates/formtools/preview.html
<p>form attr: {{ form|all_attr }}</p>
というテンプレートとテンプレートタグを用意し、各フィールドが持っている属性を調べたところ、
form attr: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__html__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_initial_value', 'as_hidden', 'as_text', 'as_textarea', 'as_widget', 'auto_id', 'css_classes', 'data', 'errors', 'field', 'form', 'help_text', 'html_initial_id', 'html_initial_name', 'html_name', 'id_for_label', 'is_hidden', 'label', 'label_tag', 'name', 'value']
という結果が得られました。
これより、auto_id
を使うことで、ManyToManyFieldのID属性を特定できそうでした。
ManyToManyFieldはformtools_categories
というIDを持つことから、
path/to/project/myapp/templatetags/field_extras.py
@register.filter def data_verbose(bound_field): if bound_field.auto_id == 'formtools_categories': result = [Category.objects.get(pk=d).name for d in bound_field.data] return ','.join(result) return bound_field.data
というformのfieldを受け取るテンプレートタグを作成し、それを
path/to/project/myapp/templates/formtools/preview.html
<table> {% for field in form %} <tr> <th>{{ field.label }}:</th> <th>auto_id: </th> <th>{{ field.label }} - readable </th> </tr> <tr> <td>{{ field.data }}</td> <td>{{ field.auto_id }}</td> <td>{{ field|data_verbose }}</td> </tr> {% endfor %} </table>
としてテンプレートで使用すれば、optionタグのテキスト内容を表示できました。
その他
FormPreviewのdoneにてリダイレクトする先のビューとテンプレートも忘れずに要しておきます。
- View
path/to/project/myapp/views.py
のArticleDetail
- Template
path/to/project/myapp/templates/myapp/article_detail.html
結果
(env) d:\Sandbox\django_form_preview_sample>python manage.py migrate (env) d:\Sandbox\django_form_preview_sample>python manage.py loaddata initial_data (env) d:\Sandbox\django_form_preview_sample>python manage.py runserver
と開発サーバを起動してみたところ、
フォーム
フォームで入力した内容の確認
確認画面の画像
確認画面のHTMLソース
<table> <tr> <th>Title:</th> <th>auto_id: </th> <th>Title - readable </th> </tr> <tr> <td>タイトル</td> <td>formtools_title</td> <td>タイトル</td> </tr> <tr> <th>Categories:</th> <th>auto_id: </th> <th>Categories - readable </th> </tr> <tr> <td>['1', '3']</td> <td>formtools_categories</td> <td>cat1,cat3</td> </tr> <tr> <th>Content:</th> <th>auto_id: </th> <th>Content - readable </th> </tr> <tr> <td>テキストエリア</td> <td>formtools_content</td> <td>テキストエリア</td> </tr>
となり、Python3.5 + Django1.9での動作確認がとれました。