/ PythonDjango

Django、インラインフォームセットを使う

概要

Django、モデルフォームセットを使うでモデルフォームセットについて説明しましたが、今回はそれの兄弟にあたるインラインフォームセットです。 モデルフォームセットと似ていますが、少し用途が違います。

前回は記事を一度に複数作成しましたが、一つの記事には任意の数の添付ファイルをつけれるようにしてみました。

from django.db import models
from django.utils import timezone


class Post(models.Model):
    title = models.CharField('タイトル', max_length=200)
    text = models.TextField('本文')
    date = models.DateTimeField('日付', default=timezone.now)

    def __str__(self):
        return self.title


class File(models.Model):
    name = models.CharField('ファイル名', max_length=255)
    src = models.FileField('添付ファイル')
    target = models.ForeignKey(
        Post, verbose_name='紐づく記事',
        blank=True, null=True,
        on_delete=models.SET_NULL
    )

通常であれば記事を作成し保存したあとに、ファイルを作成して記事に紐づけていきますが、インラインフォームセットを使うと記事を作成すると同時にファイルも作成できます。

ダウンロード

Github

自分で作ったページで表示させる

見た目

記事の作成フォームのあとに、ファイル添付のフォームがいくつか表示されます。 ファイル添付欄が表示された

forms.py

forms.inlineformset_factory関数を使います。

FileFormset = forms.inlineformset_factory(
    Post, File, fields='__all__',
    extra=5, max_num=5, can_delete=False
)

第一引数と第二引数は必須で、親となるモデルと自身を指定します。 モデルフォームセットと同様に、ファイル部分のフォームをカスタマイズした場合はform_class引数にそれを指定し、自動生成されるフォームでいいならばfieldsexclude引数を指定します。 extraは新規作成用フォームの数で、デフォルトは3です。 記事に添付できるフォームの最大件数を指定したいならばmax_num引数を指定します。 can_deleteは削除用チェックボックスが表示されます。デフォルトTrueです。

views.py

モデルフォームセットもそうですが、インラインフォームセットは特に複雑になりがちです。今回の例は2種類のフォームを扱います。はじめのうちは関数ビューで書いたほうがわかりやすいです。

def add_post(request):
    form = PostCreateForm(request.POST or None)
    context = {'form': form}
    if request.method == 'POST' and form.is_valid():
        post = form.save(commit=False)
        formset = FileFormset(request.POST, files=request.FILES, instance=post)  # 今回はファイルなのでrequest.FILESが必要
        if formset.is_valid():
            post.save()
            formset.save()
            return redirect('app:index')

        # エラーメッセージつきのformsetをテンプレートへ渡すため、contextに格納
        else:
            context['formset'] = formset

    # GETのとき
    else:
        # 空のformsetをテンプレートへ渡す
        context['formset'] = FileFormset()

    return render(request, 'app/post_form.html', context)

インラインフォームでは、どのデータに紐づくかという指定をする必要があります。それがinstance引数です。 commit=Falseな物でもいいので、この段階でモデルインスタンスを取得しておく必要があります。

formset = FileFormset(request.POST, files=request.FILES, instance=post)

今回は記事も作っていますが、他によくあるのは既存のデータをinstance引数に指定するケースです。特にrequest.userを指定するケースはよくあります。これらの場合は扱うフォームが1種類(formsetのみ)で済むので、処理がだいぶ見やすくなります。

formset = FileFormset(request.POST, files=request.FILES, instance=request.user)
formset = FileFormset(request.POST, files=request.FILES, instance=Post.objects.get(pk=hoge))

base.html

Bootstrap4のスターターテンプレートです。

<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

    <title>フォームセット</title>
  </head>
  <body>
    <div class="container mt-3">
        {% block content %}{% endblock %}
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
  </body>
</html>

post_form.html

モデルフォームセットを扱う場合と同じで、それに通常の{{ form }}が増えた感じです。forで取り出したり各フィールドを手動で取り出す場合は、Django、モデルフォームセットを使うと同様です。

<!-- ファイルをあつかうので、enctype="multipart/form-data" が必要 -->
<form action="" method="post" enctype="multipart/form-data">
    <h2>記事</h2>
    {{ form.as_p }}

    <h2>添付ファイル</h2>
    {{ formset.management_form }}
    {% for file_form in formset %}
        {{ file_form.as_p }}
        <hr>
    {% endfor %}

    {% csrf_token %}
    <button type="submit" class="btn btn-primary">送信</button>
</form>

更新処理でのインラインフォームセット

これは非常に簡単で、前述したビューよりもシンプルに作れます。

def update_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    form = PostCreateForm(request.POST or None, instance=post)
    formset = FileFormset(request.POST or None, files=request.FILES or None, instance=post)
    if request.method == 'POST' and form.is_valid() and formset.is_valid():
        form.save()
        formset.save()
        # 編集ページを再度表示
        return redirect('app:update_post', pk=pk)

    context = {
        'form': form,
        'formset': formset
    }

    return render(request, 'app/post_form.html', context)

記事と、すでに紐づけたファイルの更新用フォームが表示されていますね。 更新用インラインフォーム

admin管理サイトでインライン表示させる

人によっては、管理画面でもこのインラインフォームセットを使いたいと思うかもしれません。 これは非常に簡単に作ることができます!

admin.py

from django.contrib import admin
from .models import *


class FileInline(admin.StackedInline):
    model = File
    extra = 3


class PostAdmin(admin.ModelAdmin):
    inlines = [FileInline]


admin.site.register(Post, PostAdmin)
admin.site.register(File)  # 一応、ファイル単体の管理画面も作っておく

こんな感じになります。

admin.StackedInlineadmin.TabularInlineに変えると...

class FileInline(admin.TabularInline):
    model = File
    extra = 3

各フィールドが横一列に並びます。こちらも見た目にいいですね。