naritoブログ

【お知らせ】
・コメントで質問等をしたが返事が返ってこない場合、私はそれを見落としています。
その場合は再度コメントをするかメールをしてください(toritoritorina@gmail.com)。

・近いうちに新しいブログが作成されます。わーお!

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

約21時間前 2018年9月23日13:09
プログラミング関連
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=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)



以下のような見た目になります。ManyToManyFieldをインラインで表示したい場合は、このような見た目のものが欲しい場合になるでしょう。
通常ページでManyToManyFieldをインライン表示

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

更新処理で使う


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

以下のような見た目になります。
管理画面でManyToManyFieldをインライン表示


タグから見た記事


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

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)

名無し 約160日前 2018年4月17日9:55 返信する
勉強になりました。ありがとうございます!
名無し 約90日前 2018年6月25日21:58 返信する
こんにちは、いつも興味深く読んでいます。
このブログを読んでいて私も何か作りたくなり、簡易SNSのようなものを作ってみています。
投稿には、ユーザーがタグとカテゴリーを追加できるようにしました。
そこで質問なのですが、ajaxを利用するなりして、ユーザーがカテゴリーを選ぶとそれに関連したタグの選択肢に切り替えるようにすることは可能ですか?

例えば、フォームで旅行カテゴリーを選んだら都道府県のタグから選択できるようになり、スポーツカテゴリーを選んだらスポーツ名のタグを選択する画面に切り替える、などです。
なりと 約90日前 2018年6月26日10:06
はい、可能です。
カテゴリ、タグモデルをForeignKey等で関連付け、Ajaxを使ってもいいですし、
もしくは
<script>
var data = {
{% for category in category_list %}
{{ category.name }}: カテゴリに属するタグの一覧... ,
{% endfor %}
}
</script>
のようにして、あらかじめhtml内に定義させ、onchange等のイベントでカテゴリに属するタグを設定する、などもあります。
名無し 約89日前 2018年6月26日23:23 返信する
ajaxを使って自分でも驚くほどイメージ通りに実装できました。ありがとうございます。
カテゴリーごとのタグなのですが、タグをバラのまま置いておくと管理サイトが見にくくなるので、管理サイトでタグをカテゴリ名のフォルダーに入れたいのですが、そんなことはできますか?
djangoのビルトインのurls.pyを変えてしまわずに実装したいと思っています。
なりと 約89日前 2018年6月27日5:52
ForeignKeyで紐付いていれば、インファインフォームセットでカテゴリとそれに紐づくタグを一緒に表示できます。
https://torina.top/detail/432/#i3

ManyToManyでも似たようなことができます。
https://torina.top/detail/463/#i2