Django + django-formtoolsで、フォームの確認画面を作ってみた

Djangoでフォームの確認画面(プレビュー)を出す方法を調べてみたところ、django.contrib.formtools.FormPreviewがありました。

ただ、Django1.8よりdjango.contrib.formtoolsがライブラリdjango-formtoolsとして切り出されていました。

 
そこで、以下を参考に、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つになります。

ひな型がそれぞれ

に用意されています。

 
このテンプレートを元に、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.pyArticleDetail
  • 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

と開発サーバを起動してみたところ、

 

フォーム

f:id:thinkAmi:20160122221638p:plain

フォームで入力した内容の確認
確認画面の画像

f:id:thinkAmi:20160122221503p:plain

確認画面の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>[&#39;1&#39;, &#39;3&#39;]</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での動作確認がとれました。

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Django_form_preview_sample