既存モデルを拡張する
Python2.5 + Django1.0.2で動作確認ずみ。
Djangoで既存モデルを拡張したい場合、アプローチは2つある。
- マルチテーブル継承を使う。
- ForeignKeyで既存モデルを参照する。
マルチテーブル継承を使う
前者のマルチテーブル継承は、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
- nameに「hoge1」と指定して、Childを作成
- nameに「hoge1」と指定して、Childを作成 -> これはバリデーションではじかれる。
- nameに「hoge1」と指定して、Parentを作成 -> これもバリデーションではじかれる。
- nameに「hoge2」と指定して、Parentを作成
- 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)