naritoブログ

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

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

約70日前 2018年4月16日9:49
プログラミング関連
Django Python

概要


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=30)
    tag = models.ManyToManyField(Tag, verbose_name='タグ')

    def __str__(self):
        return self.title



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に指定することと、admin.TabularInline等のmodels属性の上書きです。ちょっと変な書き方に見えますが、これで上手く動作します。

管理サイトでの見た目


管理画面でManyToManyFieldをインライン表示


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


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

forms.py



from django import forms
from .models import Post, Tag


class PostCreateForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title',)
        widgets = {
            'title': forms.TextInput(attrs={
                'class': 'form-control',
            }),
        }


class TagSelectForm(forms.ModelForm):
    class Meta:
        model = Post.tag.through
        fields = ('tag',)
        widgets = {
            'tag': forms.Select(attrs={
                'class': 'form-control',
            }),
        }


TagInlineFormSet = forms.inlineformset_factory(
    Post, Post.tag.through, form=TagSelectForm, can_delete=False
)



PostCreateFormは、Postを追加するための通常のフォームですね。

インラインフォームは、forms.inlineformset_factoryを使って作ります。
通常のインラインフォームとルールは同じですが、Post.tag.through のような特殊な指定が必要です。
また、Bootstrap4対応したかったので、form=TagSelectFormとしています。単純な例なら、fields=('tag',)とかでも大丈夫です。
TagInlineFormSet = forms.inlineformset_factory(
    Post, Post.tag.through, form=TagSelectForm, can_delete=False
)


Bootstrap4用のTagSelecrFormですが、modelの指定を間違えないようにしましょう。
class TagSelectForm(forms.ModelForm):
    class Meta:
        model = Post.tag.through
        fields = ('tag',)
        widgets = {
            'tag': forms.Select(attrs={
                'class': 'form-control',
            }),
        }


views.py


views.pyも、通常のインラインフォーム利用時と同じになります。
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.views import generic
from .forms import PostCreateForm, TagInlineFormSet
from .models import Post


class PostCreateView(generic.CreateView):
    model = Post
    form_class = PostCreateForm
    success_url = reverse_lazy('app:index')  # 今回はトップページを作ってないのでエラーになりますが、通常であればトップページがあるでしょう。

    def form_valid(self, form):
        # 記事をまだ保存しない。タグに問題がない場合だけ、保存するため。
        post = form.save(commit=False)
        # instance=postとして、紐付ける必要があります。
        formset = TagInlineFormSet(self.request.POST, instance=post)

        # タグ達が問題なければ、記事も保存する
        if formset.is_valid():
            post.save()
            formset.save()
            return HttpResponseRedirect(self.get_success_url())  # トップページ等にリダイレクト。django.shortcutsのredirect関数等でもOK
        else:
            # タグに問題があれば、もう一度ページを表示させる。
            return self.render_to_response(self.get_context_data(form=form, formset=formset))

    def get_context_data(self, **kwargs):
        # テンプレートへformsetという名前でタグのフォームセットを渡す。
        # 記事用のformは、親のメソッドでやってくれます。
        if 'formset' not in kwargs:
            kwargs['formset'] = TagInlineFormSet(self.request.POST or None)
        return super().get_context_data(**kwargs)



通常ページでの見た目


よくできていますね。
通常ページでManyToManyFieldをインライン表示

タグ指定時に、既に上で指定したタグがあればエラーです。
同じタグを指定すると下記の重複したデータを修正してくださいと出る


UpdtaeViewで使う場合


UpdateViewで使う場合も、通常のインランフォーム利用時と同様に使えます。
違いはUpdateViewになったこと、get_context_data内でinstance=self.objectとすることです。self.objectは、親のクラスで作成されているPostモデルインスタンスです。
class PostUpdateView(generic.UpdateView):  # UpdateView
...
...
...
    def get_context_data(self, **kwargs):
        if 'formset' not in kwargs:
            # instance=self.objectとする
            kwargs['formset'] = TagInlineFormSet(self.request.POST or None, instance=self.object)
        return super().get_context_data(**kwargs)



更新の際の見た目


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

殆ど、通常のインラインフォームと同様に使えます。can_deleteによる削除チェックボックスや、max_num引数も同様です。
名無し 約69日前 2018年4月17日9:55 返信する
勉強になりました。ありがとうございます!