Related objects listing under the admin site
Sometimes you might not want to create admin inlines, but still want to link to some related data. We faced this problem in the eKe Project Management app where the related objects were often too complex to be shown as inlines. Thus we came up with a simple solution to extend the admin interface. Our solution simply gives a listing of related objects underneath the inline elements. It collects automatically all the reverse relationships ending on _set, but allows to exclude or register new relationships as well.

First of all we need a simple utility object. We had a brilliant idea for the name of this module: utils.py.
class AlreadyRegistered(Exception):
pass
class RelatedObjectLookup(object):
'''
Retrieves related objects defined by reverse relationships or given by the user.
'''
def __init__(self):
self._related = []
self._exclude = []
def register(self, relation):
'''
Add a relationship to be looked up
'''
if relation in self._exclude:
self._exclude.remove(relation)
if relation in self._related:
raise AlreadyRegistered("The relationship %s is alread registered" % relation)
self._related.append(relation)
def unregister(self, relation):
'''
Exclude a relationship from the lookup
'''
if relation in self._related:
self._related.remove(relation)
if relation in self._exclude:
pass
#raise AlreadyRegistered("The relationship %s is alread registered" % relation)
else:
self._exclude.append(relation)
def get_related_objects(self, klass):
'''
Get related objects as a dictionary of verbose_name_plural -> related_set.all()
'''
related = [field for field in dir(klass) if field.endswith('_set') and field not in self._exclude]
related.extend(self._related)
# get distinct values
related = {}.fromkeys(related).keys()
# get rid of wrong fields
for field in related:
try:
getattr(klass, field)
except AttributeError:
related.remove(field)
# sort by model name
def get_model_name_plural(field):
return getattr(klass, field).model._meta.verbose_name_plural
related = sorted(related, key=get_model_name_plural)
# put it all together
objects = []
for field in related:
try:
admin_add_url = getattr(klass, field).model.get_admin_add_url()
except AttributeError:
admin_add_url = False
objects.append({'model': getattr(klass, field).model._meta.verbose_name_plural,
'data': getattr(klass, field).all(),
'admin_add_url': admin_add_url,
})
return objects
def __call__(self, klass):
return self.get_related_objects(klass)
related_objects = RelatedObjectLookup()
This defines a simple registry class to add and retrieve related objects. To wire this up in our application we had to edit one of the admin templates. Thanks to django's template blocks finally we overwrite just the bare minimum. As we want to extend a change view, we should use its template $TEMPLATE_DIR/$YOUR_APP/$YOUR_MODEL/change_form.html is the template we should edit. Thankfully it contains a {{after_related_objects}} block that is perfect for our purpose.
{% extends "admin/change_form.html" %}
{% load i18n captureas %}
{% block after_related_objects %}
{% trans "Related objects" %}
{% for related_object in original.get_related_objects %}
{% captureas model_name_plural %}{{ related_object.model }}{% endcaptureas %}
{% blocktrans %}Related {{ model_name_plural }}{% endblocktrans %}
{% for element in related_object.data %}
{% if element.get_admin_change_url %}
- {{ element }}
{% else %}
- {{ element }}
{% endif %}
{% empty %}
- {% blocktrans %}No related {{ model_name_plural }} found{% endblocktrans %}
{% endfor %}
{% if related_object.admin_add_url %}
- {% trans "Add new" %}
{% endif %}
{% endfor %}
{% endblock %}
A couple of notes are in order here. First as we live in an international environment i18n is crucial for us. Unfortunately blocktrans can't handle non-simple variables, as a result we need the captureas template tags too. Once these are loaded we loop over {{ object.get_related_objects }}, but hey ... such a method does not exist yet! This will be the next step once we have finished explaining the template code.
In the loop we check for two more method calls {{ element.get_admin_change_url }} and {{ element.get_admin_add_url }}. As one might guess from the methods' name these provide the url to add and change a model instance. We opted for this approach because by default the RelatedObjectLookup class registers all the related objects, but by checking for these url methods we can easily filter out unnecessary objects; actually we just don't provide these methods.
But how do these methods look like and where do they live? Let's assume you have two models:
- Blog
- Post
Clearly we would like to show the related Posts for a Blog. To accomplish this we should add the following code to our two models
from utils import related_objects
class Blog(models.Model):
....
def get_related_objects(self):
return related_objects
class Post(models.Model):
....
@staticmethod
@models.permalink
def get_admin_add_url():
return ('admin:myapp_post_add',)
@permalink
def get_admin_change_url(self):
return ('admin:myapp_post_change', (self.pk,))
That's all! Of course, if we would like to add other objects we should extend utils.related_objects appropriately. We can do this either by importing it e.g. in your apps urls.py to extend the registered objects of a different app's model, or by simply extending it in the model's get_related_objects method. Actually, we are using both approaches in our project management application.
blog comments powered by Disqus