naritoブログ

このブログはPython/DjangoとBootstrap4で作成されました
Githubのソースコード

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

約144日前 2018年1月4日9:43
プログラミング関連
Django Python

概要


このまえDjango、モデルフォームセットを紹介したので、今回はインラインフォームセットを紹介します。
インラインフォームセットはどういう状況に使うのかといいますと...

まず、以下のようなモデルがあったとしましょう。
日記の一日と、それに紐づくカテゴリです。
from django.db import models
from django.utils import timezone


class Category(models.Model):
    """日記のカテゴリ"""
    name = models.CharField('タイトル', max_length=30)

    def __str__(self):
        return self.name


class Day(models.Model):
    """日記の一日となるモデル"""
    title = models.CharField('タイトル', max_length=200)
    text = models.TextField('本文')
    date = models.DateTimeField('日付', default=timezone.now)
    category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT)

    def __str__(self):
        return self.title




通常であればまずCategoryを作成し、その後Dayを作成する際に既に作成したCategoryを指定していきます。
しかし場合によっては、あるカテゴリを作成すると同時にDayも作成したくなるかもしれません。別々に作成させりゃいいじゃんと思っても、そうはいかない状況も出てきます。
Categoryの作成画面でDayも一緒に作成できるのが、インラインフォームです。
実際に使ってみます。

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



forms.py


inlineformset_factory関数を使います。第一引数と第二引数は必須です。
fieldsは、excludeやform引数を指定すれば不要です。
modelformset_factoryと違い、extraはデフォルトで3、can_deleteがデフォルトでTrueです。とりあえず、今回はcan_deleteをFalseにします。
Trueにしても以降のコードは何も変更箇所はありませんので、削除用のチェックボックスが欲しい方はTrueにしましょう。
DayInlineFormSet = forms.inlineformset_factory(
    Category, Day, fields=('title', 'text'), can_delete=False
)



views.py


モデルフォームセットとは違い、カテゴリの追加処理がまず必要になります。
なのでCreateViewでカテゴリ作成ができるようにして、それに記事のフォームセットをくっつけるアプローチにします。
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.views import generic
from .forms import DayInlineFormSet
from .models import Category
...
...
class CategoryCreateView(generic.CreateView):
    model = Category
    fields = '__all__'
    success_url = reverse_lazy('diary:index')

    def form_valid(self, form):
        # カテゴリはまだ保存せず、オブジェクトだけ取得する
        self.object = form.save(commit=False)

        # 取得したカテゴリを、instance引数に渡す
        formset = DayInlineFormSet(self.request.POST, instance=self.object)

        # 記事達が問題なければ、カテゴリを保存し、記事達も保存する
        if formset.is_valid():
            self.object.save()
            formset.save()
            return HttpResponseRedirect(self.get_success_url())
        else:
            return self.render_to_response(self.get_context_data(form=form, formset=formset))

    def get_context_data(self, **kwargs):
        if 'formset' not in kwargs:
            kwargs['formset'] = DayInlineFormSet(self.request.POST or None)
        return super().get_context_data(**kwargs)



category_form.html


カテゴリ(と記事達)を追加する、category_form.htmlは以下のようになりました。
{% extends 'diary/base.html' %}

{% block content %}
<form action="" method="POST">
  <!-- ここから、カテゴリ({{ form }}) の部分 -->
  <h2>カテゴリの追加</h2>
  {{ form.non_field_errors }}
  <table class="table table-bordered">
      {% for field in form %}
          <tr>
              <td>{{ field.label_tag }}</td>
              <td>{{ field }} {{ field.errors }}</td>
          </tr>
      {% endfor %}
  </table><!-- ここまで、カテゴリの追加 -->
  
  <!-- ここから、記事達({{ formset }})の部分 -->
  <h2>記事達の追加</h2>
  {{ formset.management_form }} 
  {% for form in formset %}
    {{ form.id }}
    {{ form.non_field_errors }}
    <table class="table table-bordered">
        {% for field in form.visible_fields %}
            <tr>
                <td>{{ field.label_tag }}</td>
                <td>{{ field }} {{ field.errors }}</td>
            </tr>
        {% endfor %}
    </table>
  {% endfor %}<!-- ここまで、記事達の追加 -->

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




今回は、forループを上手く使ってフォーム部分を作成しています。こちらがカテゴリ部分
  <!-- ここから、カテゴリ({{ form }}) の部分 -->
  <h2>カテゴリの追加</h2>
  {{ form.non_field_errors }}
  <table class="table table-bordered">
      {% for field in form %}
          <tr>
              <td>{{ field.label_tag }}</td>
              <td>{{ field }} {{ field.errors }}</td>
          </tr>
      {% endfor %}
  </table><!-- ここまで、カテゴリの追加 -->



そして、記事の部分。モデルフォームセットと扱い方は同じです。
  <!-- ここから、記事達({{ formset }})の部分 -->
  <h2>記事達の追加</h2>
  {{ formset.management_form }} 
  {% for form in formset %}
    {{ form.id }}
    {{ form.non_field_errors }}
    <table class="table table-bordered">
        {% for field in form.visible_fields %}
            <tr>
                <td>{{ field.label_tag }}</td>
                <td>{{ field }} {{ field.errors }}</td>
            </tr>
        {% endfor %}
    </table>
  {% endfor %}<!-- ここまで、記事達の追加 -->



通常ページでの見た目


ブラウザでアクセスしてみると、中々良い感じですね。
通常のページでインライン表示させた


UpdateViewで使う


カテゴリの更新でも同様にインラインフォームを使うならば、先程とほとんど同じように書けます。
class CategoryUpdateView(generic.UpdateView):
    model = Category
    fields = '__all__'
    success_url = reverse_lazy('diary:index')

    def form_valid(self, form):
        # カテゴリはまだ保存せず、オブジェクトだけ取得する
        self.object = form.save(commit=False)

        # 取得したカテゴリを、instance引数に渡す
        formset = DayInlineFormSet(self.request.POST, instance=self.object)

        # 記事達が問題なければ、カテゴリを保存し、記事達も保存する
        if formset.is_valid():
            self.object.save()
            formset.save()
            return HttpResponseRedirect(self.get_success_url())
        else:
            return self.render_to_response(self.get_context_data(form=form, formset=formset))

    def get_context_data(self, **kwargs):
        if 'formset' not in kwargs:
            kwargs['formset'] = DayInlineFormSet(self.request.POST or None, instance=self.object)
        return super().get_context_data(**kwargs)




違いはUpdateViewになったことと、get_context_dataメソッド内のDayInlineFormSetにinstance引数が追加されたことです。
重複コードが多いので、上手いこと共通化できそうですね。フォームセット用のViewやMixinを書くのも面白いでしょう。
テンプレートは、作成で使ったものがそのまま使えます。
class CategoryUpdateView(generic.UpdateView):
...
...
    def get_context_data(self, **kwargs):
        if 'formset' not in kwargs:
            kwargs['formset'] = DayInlineFormSet(self.request.POST or None, instance=self.object)



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


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

admin.py


from django.contrib import admin
from .models import Day, Category


class DayInline(admin.StackedInline):
    model = Day
    extra = 3


class CategoryAdmin(admin.ModelAdmin):
    inlines = [DayInline]


admin.site.register(Category, CategoryAdmin)
admin.site.register(Day)



admin管理サイトでの見た目


管理画面でインライン表示させた


admin.StackedInlineをadmin.TabularInlineに変えると...
class DayInline(admin.TabularInline):
    model = Day
    extra = 3



各フィールドが横一列に並びます。こちらも見た目にいいですね。
管理画面でインライン表示。1データを1列に