django: モデルのModelAdmin登録を自動化

「boilerplate=繰り返さなきゃいけないこと」を最小限にするのがフレームワークの指名だと思う。djangoのboilerplateは許せる範囲内。でもModelAdminは我慢できない。こんなのをクラスごのとにやらなきゃいけない。


class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'timestamp')
admin.site.register(Post, PostAdmin)

リストで見せるフィールドを自動的に選択しちゃえば、記述することが何もなくなってしまう。これは0行に圧縮すべきだ。とある日思い立って自動登録ライブラリを書いた。それ以来「ModelAdmin」の文字は書いていない。

手書してきたコードを自動化するばあいはlispのマクロのような機構があったら簡単そうだが、pythonなのでintrospectionでゴリゴリ掘ってやるしかない。

注意: これ使うと、modelsのインポートを揃えないと変なエラーがおきる可能性がある。
あるところでこうやって:
from foo.models import *
別のところでこすると、エラーが出ちゃうかもしれない。
import foo.models
モデルのインポートの仕方を画一化しないといけないという制約がついてしまうってことかな。
そういう時はregister_all()をコメントアウトしちゃってください。

綺麗な色つきソースコードhttp://python.pastebin.com/AWnvXqe5
念のため同じものを以下、貼っておきます。


# -*- coding: utf-8 -*-
""" model admin setup """
import os
from django.contrib import admin
import django.db.models
import settings

"""
djangoのモデルクラスをadmin画面に出すには以下のようにHogeAmdinクラスを
登録するおまじないが必要だが、毎回となえるのは面倒なので、自動登録する。

This modules automates Amdin class registration boilerplate
for django model classes.

A series of this:
class PostAdmin(admin.ModelAdmin):
list_display = ('asset', 'timestamp')
admin.site.register(Post, PostAdmin)

プロジェクト毎に以下のコードでモデルのadmin登録ができるようになる。
is replaced by this:
import model_admin_autoreg
model_admin_autoreg.register_all()

"""

def app_modules(project_root=settings.PROJECT_ROOT,
import_names=settings.INSTALLED_APPS):
""" divide apps into local ones whose models I want to auto-register
and projects that I don't what to deal with.
former is a list of modules, latter a list of paths.

appを現プロジェクトと外部のものに分割。外部のされたものは自動登録の
対象外となる。
"""
local_mods=[]
non_local_import_paths=set()
for app_name in import_names:
try:
module=__import__(app_name, fromlist=['hoge'])
except ImportError:
# complain and go on.
logging.warn("not importing %s" % app_name)
if os.path.abspath(module.__file__).startswith(project_root):
local_mods.append(module)
else:
# remember external apps.
non_local_import_paths.add(app_name)
return local_mods, non_local_import_paths

def local_model_classes(model_modules, nonlocal_module_names):
""" expand app modules to model classes, taking care to exclude
classes that appear to be non-local.

モデルモジュールをモデルクラスに拡張する。
"""
lmcs=set()
for mm in model_modules:
# ass=mm.Asset
model_classes=[ v for v in mm.__dict__.values()
if isinstance(v, django.db.models.base.ModelBase) ]
for mc in model_classes:
# again, filter for local classes
if mc.__module__ in nonlocal_module_names:
# print 'non-local: ', mc.__name__
pass
else:
lmcs.add(mc)
return lmcs

def local_model_classes_for_current_project():
""" apply local_model_classes() to the current project.
現プロジェクトのモデルクラスを発見する。
"""
mcs=local_model_classes(
*app_modules(import_names=[ "%s.models" % p
for p in settings.INSTALLED_APPS
]))
# in case from 'django.db.models import *' was done
return filter(lambda mc: mc.__name__!='Model', mcs)

this_mod=__import__(__name__)

def register_admin(model_class, field_filter):
"""
create Admin class for the model class and register an instance.
'macro' to automate this:
class PostAdmin(admin.ModelAdmin):
list_display = ('asset', 'timestamp')
admin.site.register(Post, PostAdmin)
"""

# assert issubclass(model_class, django.db.models.base.ModelBase)
# marshall all the bits
model_name=model_class.__name__
admin_name=model_name+'Admin'

# dump all model fields here.
# let caller have some control over this..
# first, examine the attrs
import django.db.models.fields
fields=model_class._meta.local_fields
#
# apply desired filtering here
# AutoField, CharField, DateTimeField, related.ForeignKey..
list_display=filter(None,[ field_filter(model_class, f) for f in fields])

# generate a subclass of Model Base named admin_name,
# having an attr list_display.
from new import classobj
admin_cls=classobj(admin_name,
(admin.ModelAdmin,),
dict(list_display=list_display))

# now register the admin class
admin.site.register(model_class, admin_cls)

# intern this class on this module. or is that necessary?
#setattr(this_mod, admin_name, admin_cls)

def register_all(field_filter=lambda c,f: f):
""" given a list of model classes, create Admin class for it and
register. field_filter controls display fields.
see example below.

現プロジェクトのモデルクラスをadmin登録する。
field_filterによって見せるフィールドを選択できる。
class, field_obj --> field_name
例えば、これでTextFieldはリストで表示されなくなる。
register_all(lambda cls,f: None if isinstance(f, TextField) else f.name)
"""
for mc in local_model_classes_for_current_project():
# in case from 'django.db.models import *' was done
if mc.__name__=='Model': # xx check type(mc)?
continue
# print 'registering:', mc.__name__
register_admin(mc, field_filter)

if __name__=='__main__':

for mc in local_model_classes_for_current_project():
print mc.__name__