Djangoで、GenericForeignKeyを使う

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

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

旧ブログ移行記事です。

概要

Djangoで、GenericForeignKeyを使います。これはForeignKeyを汎用的にしたもので、紐づくモデルが複数ある場合に便利です。

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

コメント投稿機能がついたとして、各コメントにもファイルが添付できるようにしたいと思います。Fileモデルは記事に、CommentFileモデルはコメントへ紐づきます。

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
    )

CommrntFileモデルはFileモデルとほとんど同じです。もしReplyというモデルができたとして、それに添付ファイルをつけたいならばReplyFileモデルが必要でしょうか。このような場合に、GenericFreignKeyが有効です。

GenericForeignKeyの基本

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を普通に管理画面で追加しようとする

このままだとちょっと不便ですが、インラインフォームと一緒に利用することで使いやすくなります。

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

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)

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

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

通常のページで使う

通常のページでこれらを使う場合は、インラインフォームと同じ流れです。 まず、forms.pyにでも定義します。

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

FileInlineFormSet = generic_inlineformset_factory(
    File, form=FileCreateForm, 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 = FileFormset(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'] = FileFormset()

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

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

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

紐づくFileを全て取り出す

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

import os
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 %}
Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする