I played around with this again this morning. I think I have something which I’m somewhat happy with.
I ended up adapting a package noted in the code comments below. It allows for onetoone and onetomany relations to be linked to an admin class.
I’m using this as a mixin for admin classes which I want to add actions or other customizations to. Or otherwise, registering all models via a loop with this as the base class. The result is that any registered model can have all of its related models navigated to, and avoids needing to write a heap of custom inlines.
It’s been noted here that issues will arise if the related models aren’t registered. But perhaps some checks can be added into this in order to prevent unregistered relations from being included.
I’m also seeing this as being quite messy right now. I think it in-fact makes more sense to strip the admin-relation-links package back out.
class ModelAdminAutoMixin(admin.ModelAdmin):
"""
https://github.com/gitaarik/django-admin-relation-links
"""
def _add_admin_field(self, field_name, func):
if not hasattr(self, field_name):
setattr(self, field_name, func)
if field_name not in self.readonly_fields:
self.readonly_fields += (field_name,)
def _add_change_link(self, model_field_name, admin_field_name):
def make_change_link(model_field_name):
def func(instance):
return self._get_change_link(instance, model_field_name, admin_field_name)
self.decorate_link_func(func, model_field_name)
return func
self._add_admin_field(admin_field_name, make_change_link(model_field_name))
def _get_change_link(self, instance, model_field_name, admin_field_name):
try:
target_instance = getattr(instance, model_field_name)
except Exception:
return
return get_link_field(
reverse(
'{}:{}_{}_change'.format(
self.admin_site.name,
target_instance._meta.app_label,
target_instance._meta.model_name
),
args=[target_instance.pk]
),
self.link_label(admin_field_name, target_instance)
)
def _add_changelist_link(self, model_field_name, admin_field_name):
def make_changelist_link(model_field_name):
def func(instance):
return self._get_changelist_link(instance, model_field_name)
self.decorate_link_func(func, model_field_name)
return func
self._add_admin_field(admin_field_name, make_changelist_link(model_field_name))
def _get_changelist_link(self, instance, model_field_name):
try:
target_instance = getattr(instance, model_field_name)
except Exception:
return
def get_url():
return reverse(
'{}:{}_{}_changelist'.format(
self.admin_site.name,
*self._get_app_model(instance, model_field_name)
)
)
def get_lookup_filter():
return instance._meta.get_field(model_field_name).field.name
def get_label():
return target_instance.model._meta.verbose_name_plural.capitalize()
return get_link_field(
'{}?{}={}'.format(get_url(), get_lookup_filter(), instance.pk), get_label()
)
def _get_app_model(self, instance, model_field_name):
model_meta = getattr(instance, model_field_name).model._meta
app = model_meta.app_label
model = model_meta.model_name
return app, model
def decorate_link_func(self, func, model_field_name):
func.short_description = model_field_name.replace('_', ' ').capitalize()
try:
field = self.model._meta.get_field(model_field_name)
except:
pass
else:
if hasattr(field.related_model._meta, 'ordering') and len(field.related_model._meta.ordering) > 0:
func.admin_order_field = '{}__{}'.format(
field.name,
field.related_model._meta.ordering[0].replace('-', '')
)
def link_label(self, admin_field_name, target_instance):
label_method_name = '{}_label'.format(admin_field_name)
if hasattr(self, label_method_name):
return getattr(self, label_method_name)(target_instance)
return str(target_instance)
def __init__(self, model, admin_site):
list_filter_threshold = 10
list_filter_type_includes = (
DateField,
DateTimeField,
ForeignKey,
BooleanField,
)
list_display_threshold = 10
list_display_type_includes = (
DateField,
DateTimeField,
ForeignKey,
BooleanField,
CharField,
)
search_includes = [
'name',
'slug',
'title',
]
date_hierarchy_includes = [
'joined_at',
'updated_at',
'created_at',
'modified_at',
]
self.list_display = ()
self.list_filter = ()
self.search_fields = ()
self.readonly_fields = ()
self.date_hierarchy = None
fields = [
f for f in model._meta.fields
if not (f.many_to_one or f.one_to_one)
]
for field in fields:
if field.primary_key or isinstance(field, list_display_type_includes) and \
len(self.list_display) < list_display_threshold:
self.list_display += (field.name,)
if isinstance(field, list_filter_type_includes) and len(self.list_filter) < list_filter_threshold:
self.list_filter += (field.name,)
if field.name in search_includes:
self.search_fields += (field.name,)
if field.name in date_hierarchy_includes and not self.date_hierarchy:
self.date_hierarchy = field.name
change_links = [
f.name for f in model._meta.get_fields()
if f not in model._meta.fields and f.one_to_one
]
for model_field_name, admin_field_name in parse_field_config(change_links):
self._add_change_link(model_field_name, admin_field_name)
changelist_links = [
f.name for f in model._meta.get_fields()
if f not in model._meta.fields and f.one_to_many
]
for model_field_name, admin_field_name in parse_field_config(changelist_links):
self._add_changelist_link(model_field_name, admin_field_name)
super().__init__(model, admin_site)