Django、GenericForeignKeyの使い方について

2018-10-18 / PythonDjango

概要

Djangoでフォームセットを使うシリーズの1つです。今回はGenericForeignKeyを使います。これはForeignKeyを汎用的にしたもので、紐づくモデルが複数ある場合に便利です。Githubにソースコードがあるので、ダウンロードしたい方はクローンしてください。

Django、インラインフォームセットの基本的な使い方では、記事と添付ファイルのモデルを定義しました。

from django.db import models


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
    )

    def __str__(self):
        return self.name

コメント投稿機能がついたとして、各コメントにもファイルが添付できるようにしたいと思います。例えば次のようになるでしょう。

class Comment(models.Model):
    text = models.TextField('本文')
    target = models.ForeignKey(Post, on_delete=models.CASCADE)


class CommentFile(models.Model):
    name = models.CharField('ファイル名', max_length=255)
    src = models.FileField('添付ファイル')
    target = models.ForeignKey(
        Comment, verbose_name='紐づくコメント',
        blank=True, null=True,
        on_delete=models.SET_NULL
    )

Fileモデルは記事に、CommentFileモデルはコメントへ紐づきます。しかしCommrntFileモデルはFileモデルとほとんど同じです。更にReplyというモデルができたとして、それに添付ファイルをつけたいならばReplyFileモデルが必要でしょうか。似たようなモデルが3つできてしまいます。このような場合に、GenericFreignKeyが有効です。

モデル

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

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
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 Comment(models.Model):
    text = models.TextField('本文')
    target = models.ForeignKey(Post, on_delete=models.CASCADE)


class File(models.Model):
    name = models.CharField('ファイル名', max_length=255)
    src = models.FileField('添付ファイル')
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    def __str__(self):
        return self.name

content_typeでモデルの種類、object_idでデータのプライマリキー、そしてcontent_objectはそれらを統合する役割を持っています。

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

ここまでで、ファイルを管理画面から追加しようとすると以下のようになります。 GenericForeignKeyを普通に管理画面で追加しようとする

このままだとちょっと不便ですが、後で説明するgeneric_inlineformset_factoryを利用することで使いやすくなります。

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

GenericTabularInlineクラスを使うことで、非常に便利に使えるようになります。

from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline
from .models import Post, File, Comment


class FileInline(GenericTabularInline):
    model = File


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


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


admin.site.register(Comment, CommentAdmin)
admin.site.register(Post, PostAdmin)
admin.site.register(File)

記事の作成画面です。記事の添付ファイルがインライン表示されてます。

コメントの追加画面でも、同様にコメントへの添付ファイルがインライン表示されます。

通常のページで使う

通常のページでGenericFreignKeyを便利に使いましょう。まず、forms.pyにでも定義します。

from django import forms
from django.contrib.contenttypes.forms import generic_inlineformset_factory
from .models import Post, File


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
        fields = '__all__'


FileInlineFormSet = generic_inlineformset_factory(
    File, fields='__all__', can_delete=False, extra=3,
)

FileInlineFormSetがファイルをインラインで表示するためのフォームセットで、generic_inlineformset_factoryを使います。通常のinlineformset_factoryと違い、モデルの指定が1つだけで済みます。それ以外はおおむね同じです。

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 = FileInlineFormSet(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'] = FileInlineFormSet()

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

ちゃんと記事への添付ファイルが表示されています。

コメントへのファイル添付も同様の流れで行えます。

紐づくFileを全て取り出す

通常のForeignKeyで、related_name引数を指定していない場合は{% for file in post.set_file.all %}のようにして取り出せます。GenericForeignKeyの場合は作業が1つ増えます。 models.pyを少し変更します。

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation  # 増えた
..
...
class Post(models.Model):
    title = models.CharField('タイトル', max_length=200)
    text = models.TextField('本文')
    date = models.DateTimeField('日付', default=timezone.now)
    files = GenericRelation('File')  # 増えた

    def __str__(self):
        return self.title
...
...

GenericRelationを使います。

files = GenericRelation('File')  # 増えた

テンプレートファイルでは、以下のようにして取り出せるようになります。

{% for file in post.files.all %}
    {{ file }}<br>
{% endfor %}

この記事の関連記事

Djangoでフォームセットを使うシリーズ

2018-10-17 / PythonDjangoシリーズ・まとめ

- Djangoにはフォームセットという、複数のフォームを一括で扱うための機能があります。いくつか種類があり、様々な機能があるので、それらを紹介していきます。

Django、インラインフォームセットの基本的な使い方

2018-10-18 / PythonDjango

- Djangoでフォームセットを使うシリーズの1つです。今回はインラインフォームセットについてです。記事に添付するファイルがあったとして、記事の作成時に一緒にファイルを複数作ることができるようになります。

コメント欄

記事にコメントする

まだコメントはありません。