既存モデルを拡張する

Python2.5 + Django1.0.2で動作確認ずみ。

Djangoで既存モデルを拡張したい場合、アプローチは2つある。

マルチテーブル継承を使う

前者のマルチテーブル継承は、Django1.0からの機能でかなり便利。ほとんどの問題はこれで解決するはず。ただし1つ問題があって、既存モデルでunique=Trueなフィールドがあると困ったことになる。(似たような問題もあるけど、少し違う?)

たとえばこういうモデルを定義して、管理サイトから編集する場合。

# -*- encoding: utf-8 -*-
# app.models.py
from django.db import models

class Parent(models.Model):
    name = models.CharField(u'名前', max_length=255, unique=True)

class Child(Parent):
    pass
  1. nameに「hoge1」と指定して、Childを作成
  2. nameに「hoge1」と指定して、Childを作成 -> これはバリデーションではじかれる。
  3. nameに「hoge1」と指定して、Parentを作成 -> これもバリデーションではじかれる。
  4. nameに「hoge2」と指定して、Parentを作成
  5. nameに「hoge2」と指定して、Childを作成 -> IntegrityErrorが発生する。

最後のIntegrityErrorを発生させないためには、ModelFormで以下のようにチェックするしかない。

# -*- encoding: utf-8 -*-
# app.forms.py
from django.forms import ModelForm, ValidationError
from app.models import Parent

class ParentForm(ModelForm):
    class Meta:
        model = Parent

    def clean_name(self):
        cleaned = self.cleaned_data['name']

        try:
            newobj = self.save(commit=False)
            qs = Parent.objects.exclude(id=newobj.id).filter(name=newobj.name)
        except:
            qs = Parent.objects.none()

        if qs:
            raise ValidationError(u'%sは登録済みです。' % cleaned)

        return cleaned

ForeignKeyで既存モデルを参照する

後者のForeignKeyで既存モデルを参照する方法は、以前からある方法でお手軽に使える。この方法で困るのは、管理サイトでの扱いが厄介な点。扱い方には2つある。

  • ModelAdminを使って、既存モデルとは独立して扱う。
  • InlineModelAdminを使って、既存モデルと同じページで扱う。

前者はお手軽に使えるのが利点。ただし、管理サイトで参照する既存モデルを変更できてしまうので、既存モデルの拡張という意味で使うのは危なっかしい。editable=Falseにすれば変更できないが、コードで生成するしか方法がなくなってしまう。生成時だけ指定できて、以降は変更できないようにすればいいんだろうけど・・・
後者は、既存モデルのModelAdminのinlinesに拡張モデルのInlineModelAdminを指定する必要があり、既存モデルのModelAdminが修正できない場合に困る。

ということで、既存モデルのModelAdminを修正せずに、既存モデルと同じページで扱う方法をメモしておく。

たとえばこんなモデルがあったとして。

# -*- encoding: utf-8 -*-
# app.models.py
from django.db import models

class Parent(models.Model):
    name = models.CharField(u'名前', max_length=255, unique=True)

class Child(models.Model):
    original = models.ForeignKey(Parent, unique=True)
    remarks = models.CharField(u'備考', max_length=255)

こんなユーティリティモジュールを定義する。

# -*- encoding: utf-8 -*-
# adminutils.py
from django.contrib import admin

def append_inlines(model, inline_admin, show_inline=True):
    """ 管理サイトでインラインに編集できるように、指定したモデルのModelAdminクラスを修正して、
        指定したInlineModelAdminを追加登録します。InlineModelAdminに「short_description」属性を
        持つメソッドがある場合、ModelAdminクラスのlist_displayフィールドにそのメソッドを追加します。
    """
    if model in admin.site._registry:
        admin_obj = admin.site._registry[model]
        inline_instance = inline_admin(model, admin_obj.admin_site)
        if show_inline:
            admin_obj.inline_instances.append(inline_instance)

        for field in dir(inline_instance):
            candidate = getattr(inline_instance, field)
            if not callable(candidate) or not hasattr(candidate, 'short_description'):
                continue

            setattr(admin_obj, field, candidate)
            admin_obj.list_display += (field,)

def register2admin(modelclass, adminclass):
    """ 管理サイトにモデルを登録します。
    """
    try:
        admin.site.unregister(modelclass)
    except:
        pass
    admin.site.register(modelclass, adminclass)

class CachedInlineModelAdmin(admin.StackedInline):
    """ 拡張モデルをキャッシュできるようにしたInlieModelAdminです。
    """
    max_num = 1
    min_num = 1

    def _get_cache(self, obj):
        """ 指定されたオブジェクトから、拡張モデルのキャッシュを取得します。
            キャッシュが存在しない場合、クラスに定義したmodelをキーに取得した拡張モデルを
            キャッシュとしてセットします。
        """
        try:
            meta = self.model._meta
            context = dict(modelname=meta.object_name.lower(), labelname=meta.app_label.lower())
            cachename = u'__%(labelname)s_%(modelname)s_cache' % context

            if not hasattr(obj, cachename):
                criteria = {}
                for field in [f for f in meta._fields() if hasattr(getattr(f, 'rel'), 'to')]:
                    if field.rel.to == obj.__class__:
                        criteria[field.name] = obj
                        break

                setattr(obj, cachename, self.model.objects.get(**criteria))

            return getattr(obj, cachename, None)
        except:
            return None

使い方はこんな感じ。管理サイトで既存モデルParentを作成するとき、Childの属性もインラインで編集できるようになる。

# -*- encoding: utf-8 -*-
# app.admin.py
from django.contrib import admin

from app import models
import adminutils

class ParentAdmin(admin.ModelAdmin):
    list_display = ('name',)
    model = models.Parent
adminutils.register2admin(models.Parent, ParentAdmin)

class ChildAdmin(adminutils.CachedInlineModelAdmin):
    model = models.Child

    def remarks(self, obj):
        return self._get_cache(obj).remarks
    remarks.short_description = u'備考'
adminutils.append_inlines(models.Parent, ChildAdmin)