Djangoで、ManyToManyFieldをインラインで表示

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年11月22日21:14に更新(約19日前)
2018年10月18日5:45に作成(約54日前)

旧ブログ移行記事です。

概要

ManyToManyFieldをインラインで表示するサンプルです。admin管理サイトでのインライン表示と、通常のページでインライン表示させていきます。

models.py

以下のような、シンプルなモデルです。記事があり、複数のタグを指定できる、という関係ですね。

from django.db import models


class Tag(models.Model):
    name = models.CharField('タグ名', max_length=30)

    def __str__(self):
        return self.name


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

    def __str__(self):
        return self.title

記事から見たタグ

通常のページでインライン表示させる

凡その流れは、通常のインラインフォームセットの使い方と同じです。

forms.pyを以下のようにします。

from django import forms
from .models import Post, Tag


class PostCreateForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'

    class Meta:
        model = Post
        exclude = ('tag',)


TagInlineFormSet = forms.inlineformset_factory(
    Post, Post.tag.through, fields='__all__', can_delete=False
)

PostCreateFormは、Postを追加するためのBootstrap4対応フォームです。excludeにタグを指定しておきます。インラインで追加するので、記事のフォームには含めないのです。

インラインフォームは、forms.inlineformset_factory関数を使って作ります。 通常のインラインフォームとルールは同じですが、Post.tag.throughのような特殊な指定が必要です。

TagInlineFormSet = forms.inlineformset_factory(
    Post, Post.tag.through, fields='__all__', can_delete=False
)

views.pyも、通常のインラインフォームセット利用時と変わりません。

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 = TagInlineFormSet(request.POST, instance=post)
        if formset.is_valid():
            post.save()
            formset.save()
            return redirect('app:index')

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

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

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

以下のような見た目になります。

タグ指定時に、既に上で指定したタグがあればエラーです。

更新処理で使う

UpdateViewで使う場合も、通常のインランフォーム利用時と同様に使えます。

def update_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    form = PostCreateForm(request.POST or None, instance=post)
    formset = TagInlineFormSet(request.POST 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)

更新なので、既に紐付いたタグも表示されてますね。

殆ど、通常のインラインフォームと同様に使えます。can_deleteによる削除チェックボックスや、max_num引数も同様です。

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

これは非常に簡単に実装できます。admin.pyを以下のようにしましょう。

from django.contrib import admin
from .models import Post, Tag


class TagInline(admin.TabularInline):
    model = Post.tag.through


class PostAdmin(admin.ModelAdmin):
    inlines = [TagInline]
    exclude = ('tag',)


admin.site.register(Post, PostAdmin)
admin.site.register(Tag)

管理サイトでインライン表示させたい場合と殆ど同じです。 違いはexcludeにタグを指定することと、models属性の上書きです。ちょっと変な書き方に見えますが、これで上手く動作します。

以下のような見た目になります。

タグから見た記事

今度は今までの逆側、タグの作成・更新時に記事を指定できるようにしてみましょう。

admin管理サイト

models属性の書き方にだけ注意をしましょう。

class PostInline(admin.TabularInline):
    model = Post.tag.through


class TagAdmin(admin.ModelAdmin):
    inlines = [PostInline]


admin.site.register(Tag, TagAdmin)

ちゃんと記事が選択できるようになりました。

通常ページ

forms.pyです。 Tagはフィールドが1つで、わざわざBootstrap4対応したフォームを作るのも面倒だったので、modelform_factory関数で定義しました。この関数はモデルからモデルフォームを生成します。PostInlineFormSetでは、Post.tag.throughとすることを忘れないようにしましょう。

TagCreateForm = forms.modelform_factory(Tag, fields='__all__')

PostInlineFormSet = forms.inlineformset_factory(
    Tag, Post.tag.through, fields='__all__', can_delete=False
)

views.py、先ほどと同じ流れです。

def add_tag(request):
    form = TagCreateForm(request.POST or None)
    context = {'form': form}
    if request.method == 'POST' and form.is_valid():
        tag = form.save(commit=False)
        formset = PostInlineFormSet(request.POST, instance=tag)
        if formset.is_valid():
            tag.save()
            formset.save()
            return redirect('app:index')

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

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

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


def update_tag(request, pk):
    tag = get_object_or_404(Tag, pk=pk)
    form = TagCreateForm(request.POST or None, instance=tag)
    formset = PostInlineFormSet(request.POST or None, instance=tag)
    if request.method == 'POST' and form.is_valid() and formset.is_valid():
        form.save()
        formset.save()
        # 編集ページを再度表示
        return redirect('app:update_tag', pk=pk)

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

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

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする