from types import ModuleType
from django.contrib import admin
from django.contrib.admin.util import quote
from django.core.urlresolvers import reverse, NoReverseMatch
from django.db.models import ForeignKey, OneToOneField, Count
from django.db.models.base import ModelBase
def _get_admin_change_url(field):
'''Return function to generate admin change view url for a related object.
@param field: field pointing to a related object
@type field: ForeignKey or OneToOneField
'''
related_model = field.related.parent_model
def f(self, obj):
link_args = getattr(obj, field.attname)
if link_args is None:
return u'(None)'
# we could use field.name to output __unicode__() of the related object,
# but that would require to prefetch related objects, which can be slow
link_text = getattr(obj, field.attname)
try:
url = reverse('admin:%s_%s_change' %
(related_model._meta.app_label, related_model._meta.module_name),
args=[quote(link_args)])
except NoReverseMatch:
return link_text
return u'%s' % (url, link_text)
f.allow_tags = True
f.short_description = field.name
return f
def _get_admin_changelist_url(field):
'''Return function to generate admin changlelist view url for the related
objects.
@param field: field pointing to the related objects
@type field: ManyToManyField
'''
related_model = field.related.parent_model
def f(self, obj):
link_cond = '%s=%s' % (field.related_query_name(), quote(obj.pk))
link_text = u'%s (%s)' % (field.name.title(), getattr(obj, '%s__count' % field.name))
try:
url = reverse('admin:%s_%s_changelist' %
(related_model._meta.app_label, related_model._meta.module_name))
except NoReverseMatch:
return link_text
return u'%s' % (url, link_cond, link_text)
f.allow_tags = True
f.short_description = field.name
return f
def _get_admin_queryset(admin_class, count_field_names):
'''Return function to generate queryset to efficiently fetch counts()
of related objects.
'''
counts = map(Count, count_field_names)
def queryset(self, request):
qs = super(admin_class, self).queryset(request)
if counts:
qs = qs.annotate(*counts)
return qs
return queryset
def autoregister_admin(module, exclude=None, model_fields=None,
admin_fields=None):
'''
@param module: module containing django.db.models classes
@type module: str or __module__
If you are providing str, use absolute path.
@param exclude: list of classes to exclude from auto-register
@type exclude: iterable of strings or None
@param model_fields: dictionary of additional fields for list_display
{'model name': [field1, field2, ...]}
@type model_fields: dict or None
@param admin_fields: dictionary of additional admin fields
{'model name': {name: value, ...}}
@type admin_fields: dict or None
'''
exclude = exclude or []
model_fields = model_fields or {}
admin_fields = admin_fields or {}
if isinstance(module, basestring):
module = __import__(module, fromlist=[module.split('.')[-1]])
elif not isinstance(module, ModuleType):
raise TypeError('invalid type of argument `module`, expected `str` or '
'`ModuleType`, got %s.' % type(module))
# collect the models to register
models = []
for model in module.__dict__.values():
if (isinstance(model, ModelBase) and
model.__module__ == module.__name__ and
not model._meta.abstract and
model.__name__ not in exclude):
models.append(model)
# for each model prepare an admin class `Admin`
for model in models:
admin_class = type('%sAdmin' % model.__name__, (admin.ModelAdmin,), dict())
# list pk as the first value
admin_class.list_display = [model._meta.pk.name]
# list all the other fields
for field in model._meta.fields:
if field == model._meta.pk:
continue
admin_field_name = field.name
# create link for related objects
if isinstance(field, (ForeignKey, OneToOneField)):
admin_field_name += '_link'
setattr(admin_class, admin_field_name, _get_admin_change_url(field))
admin_class.list_display.append(admin_field_name)
count_field_names = []
for field in model._meta.many_to_many:
count_field_names.append(field.name)
admin_field_name = field.name + '_link'
setattr(admin_class, admin_field_name, _get_admin_changelist_url(field))
admin_class.list_display.append(admin_field_name)
# add custom model fields
for name in model_fields.get(model.__name__, []):
admin_class.list_display.append(name)
# prefetch related fields
admin_class.queryset = _get_admin_queryset(admin_class, count_field_names)
# add custom admin fields
for (name, value) in admin_fields.get(model.__name__, {}).iteritems():
setattr(admin_class, name, value)
try:
admin.site.register(model, admin_class)
# pass gracefully on duplicate registration errors
except admin.sites.AlreadyRegistered:
pass