Doing more with the Django admin

Three ways to customize this powerful application to suit your needs

The Django admin

Django offers many features to prospective developers: a mature standard library, an active user community, and all the benefits of the Python language. While other Web frameworks can make similar claims, Django's unique asset is its built-in administration application — the admin.

The admin provides advanced Create-Read-Update-Delete (CRUD) functionality out of the box, eliminating hours of repetitive work. This is key for many Web applications in development, when programmers can quickly explore their database models, and in deployment, when nontechnical end users can use the admin to add and edit site content.

In the real world, there's always some customization to be performed. The Django documentation provides lots of guidelines for reskinning the basic look and feel of the admin, and Django itself includes some simple methods to override a subset of admin behavior. What if you need to do more? Where do you start? This article provides some guidelines for advanced admin customization.

A quick tour of the admin

Most Django developers are familiar with the default features of the admin. To quickly review, start with enabling the admin in Listing 1, by editing the top-level urls.py.

Listing 1. Enabling the admin in urls.py

from django.conf.urls.defaults import * # Uncomment the next two lines to enable the admin: from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', # Uncomment the next line to enable the admin: (r'^admin/(.*)', admin.site.root), )

Software versions used in this article Django V1.0.2

SQLite V3

Python V2.4-2.6 (Django does not yet support Python V3)

IPython (for the sample output) The Django Object-Relational Mapper (ORM) supports many database back ends, but SQLite is the easiest to install and comes with many operating systems. These examples should work with any back end. For a list of databases supported by Django, see Related topics. Django provides a handy shortcut to setting up a working environment in standalone code: running python manage.py shell . All code samples in this article presuppose that the environment has been invoked in this way. In Django lingo, the following is assumed: This is a Django project called more_with_admin.

The more_with_admin project contains an application called examples. The examples application models a basic blog-like system of documents and zero or more comments on those documents. All command-line examples are from the project root—the main more_with_admin directory.

You also need to add the django.contrib.admin application to settings.INSTALLED_APPS .

Before going further, anyone planning to extensively customize the admin is advised to become familiar with the source code. For operating systems that support shortcuts or symlinks, it can be useful to make one to the admin application.

The admin lives inside the Django package. Assuming it was installed with setuptools, the admin is in site-packages under django/contrib/admin. Here is an example of a symbolic link from a project to the Django admin source you can customize based on your operating system and the location of your Django installation for easy copying and reference:

$ ln -s /path/to/Python/install/site-packages/django/contrib/admin admin-source

The admin.autodiscover() method iterates through each application in settings.INSTALLED_APPS and looks for a file called admin.py. This is usually placed in the top level of the application directory, at the same level as models.py.

The examples application needs a models.py, provided in Listing 2. A corresponding admin.py is shown below.

Listing 2. A sample models.py for this application

from django.db import models class Document(models.Model): '''A Document is a blog post or wiki entry with some text content''' name = models.CharField(max_length=255) text = models.TextField() def __unicode__(self): return self.name class Comment(models.Model): '''A Comment is some text about a given Document''' document = models.ForeignKey(Document, related_name='comments') text = models.TextField()

At this point, you can invoke the admin by running the Django development server:

python manage.py runserver

The admin is available at the default location of http://localhost:8000/admin/. After logging in, you see the basic admin screen shown below.

Figure 1. The basic Django admin screen

Changing code in admin.py Unlike other files in a Django application, if you make changes to admin.py using the Django development Web server, you may need to manually restart the server.

Note that your models are not available yet because you haven't created an admin.py. Listing 3 demonstrates code that allows you to work with the models in the admin.

Listing 3. A sample admin.py

from django.contrib import admin from more_with_admin.examples import models class DocumentAdmin(admin.ModelAdmin): pass class CommentAdmin(admin.ModelAdmin): pass admin.site.register(models.Document, DocumentAdmin) admin.site.register(models.Comment, CommentAdmin)

Now when you reload the main page in the admin, you see the new models available for use, as shown below.

Figure 2. The Django admin ready to support custom models

Customizing the admin model pages

Directory names in the admin folders Note that I use the lowercase names of the models. This is in accordance with how the normal admin pages work when generating URLs. Django calls these URL-friendly forms slugs. If you are unsure what the correct slug is for a given model, explore the admin first before creating your directories and take note of the names that appear in the URL.

The key to understanding how to customize the admin without hacking the Django source is to remember that the admin is an ordinary Django application like any other. Foremost, this means that the Django template inheritance system applies.

Django's template search order always prioritizes your own project's templates over any system ones. In addition, the admin attempts to search for hard-coded templates that match each model before resorting to the defaults. This provides an entry point for easy customization.

First, make sure Django is going to look in your template directories by editing the project's settings.py.

Listing 4. Editing settings.py to look in your template directories

TEMPLATE_DIRS = ( "/path/to/project/more_with_admin/templates", "/path/to/project/more_with_admin/examples/templates", )

Then create the following directories in your project:

$ mkdir templates/admin/examples/document/ $ mkdir templates/admin/examples/comment/

The special behavior of the Django admin will check for a directory with your application's name (here, examples ), then the name of the model ( document and comment ) before using the system templates to render that administration page.

Overriding a single model add/edit page

The name of the page that the admin uses for adding and editing a model instance is change_form.html. Start by creating a page called templates/admin/examples/document/change_form.html in the Document model directory and put a Django template inheritance line in it: {% extends "admin/change_form.html" %} .

Now you're ready to customize. Take some time to become familiar with the contents of the real admin/change_form.html. It's reasonably well organized into template blocks you can override, but some customizations may require wholesale copying of blocks. Nevertheless, using block-based template overrides is always preferable to copying the entire page.

What kinds of customizations might you want to do to the add/edit page? Maybe for each Document in the system you'd like to show a preview of the five most recent comments.

First, create some sample content.

Listing 5. Using the Django shell to create a sample Document with several comments

$ python manage.py shell In [1]: from examples import models In [2]: d = models.Document.objects.create(name='Test document', text='This is a test document.') In [3]: for c in range(0, 10): ...: models.Comment.objects.create(text='Comment number %s' % c, document=d)

Now the admin list page shows one Document . Select that Document to bring up the default add/edit page shown below.

Figure 3. Default add/edit page featuring a Document

Note that the related comments aren't shown in any way. The standard way to show related models in the admin is to use the powerful Inline classes. Inline classes allow admin users to edit or add multiple related models on a single page. To see inlines in action, edit the application admin.py to match Listing 6.

Listing 6. Adding the related model comment to the Document admin

from django.contrib import admin from more_with_admin.examples import models class CommentInline(admin.TabularInline): model = models.Comment class DocumentAdmin(admin.ModelAdmin): inlines = [CommentInline,] class CommentAdmin(admin.ModelAdmin): pass admin.site.register(models.Document, DocumentAdmin) admin.site.register(models.Comment, CommentAdmin)

Figure 4 shows the new add/edit page after adding the TabularInline control.

Figure 4. Document add/edit page after the comment model is added as an Inline

This is certainly powerful, but it may be overkill if you just want to see a quick preview of the comments.

You can take two approaches here. One is to edit the HTML widgets associated with the inline using the Django admin widget interface; the Django documentation describes widgets in detail. The other approach is to modify the add/edit template directly. This approach is most useful when you don't want to use any admin-specific features at all.

If you don't want to allow any editing of the comments (perhaps because the users don't have sufficient permissions), but you do want them to be able to see the comments, you can modify change_form.html.

Variables provided by the Django admin

To add functionality to a model instance page, you need to know what data is already available from the admin. The two key variables are described below.

Table 1. Variables needed for customizing an admin template

Variable Description object_id This is the primary key for the object being edited. If you are customizing a specific instance page (such as Document), this is all you should need. content_type_id If you are overriding multiple types of model pages, use this to query the ContentTypes framework to get the name of the model. See Related topics for more information about content types.

Creating a template tag for inclusion in the admin page

Listing the related comments requires code that can't be entered directly into a Django template. The best solution for this is to use a template tag. First, create the template tag directory and __init__.py file:

$ mkdir examples/templatetags/ $ touch examples/templatetags/__init__.py

Create a new file called examples/templatetags/example_tags.py and add the code shown below.

Listing 7. Template tag for retrieving the comments for a given Document ID

from django import template from examples import models register = template.Library() @register.inclusion_tag('comments.html') def display_comments(document_id): document = models.Document.objects.get(id__exact=document_id) comments = models.Comment.objects.filter(document=document)[0:5] return { 'comments': comments }

Because this is an inclusion tag, you need to create the corresponding template file: comments.html. Edit the examples/templates/comments.html file and enter the code from Listing 8.

Listing 8. Template for displaying a set of comment previews

{% for comment in comments %} <blockquote>{{ comment.text }}</blockquote> {% endfor %}

Now it's time to add this into the admin page. Comment out the references to CommentInline in admin.py and make the changes shown in Listing 9 to your local version of change_form.html.

Listing 9. Including the template tag in the add/edit page

{% extends "admin/change_form.html" %} {% load example_tags %} {% block after_field_sets %} {% if object_id %}{% display_comments object_id %}{% endif %} {% endblock %}

It's important to check for the existence of object_id before attempting to use it because change_form.html is also used for creating new instances, in which case object_id is not yet available. The after_field_sets block is just one of many that are provided as extension points in the admin. Consult the change_form.html source page for others.

Figure 5 shows the updated form.

Figure 5. Document add/edit page after the custom template tag is included

Modifying admin behavior

Template overrides can only do so much. What if you want to change the actual flow and behavior of the admin? Hacking the source is one possibility, but that locks you in to the specific version of Django that you're using at the time of the update.

Overriding AdminModel methods

By default, clicking Save in the admin returns the user to the list page. Usually, that's fine, but what if you want to go directly to a preview page for an object that is outside of the admin? This is a common use case when developing a content management system (CMS).

Providing the method Listing 10 assumes that the Document has been modified to include a get_absolute_url() method, which is the recommended way for a Django model to specify its canonical representation. If this is specified, the Django admin also supplies a useful View on site button on each page for that model.

Most of the functionality in the admin application is attached to the admin.ModelAdmin class. This is the class that objects inherit from in admin.py. There are many, many public methods you can override. Check the source in admin-source/options.py for the class definition.

There are two ways to change the behavior of the Save button: You can override admin.ModelAdmin.response_add , which is responsible for the actual redirection after a save, or you can override admin.ModelAdmin.change_view . The latter is somewhat simpler.

Listing 10. Overriding the page users are directed to after a save event

class DocumentAdmin(admin.ModelAdmin): def change_view(self, request, object_id, extra_context=None): result = super(DocumentAdmin, self).change_view(request, object_id, extra_context) document = models.Document.objects.get(id__exact=object_id) if not request.POST.has_key('_addanother') and not request.POST.has_key('_continue'): result['Location'] = document.get_absolute_url() return result

Now when users click Save, they are redirected to the preview page, rather than to the list page showing all Documents.

Adding features to the admin with signals

Signals are an under-used feature in Django that improve the modularity of your code. Signals define events, such as saving a model or loading a template, that function anywhere the Django project can listen for and respond to. This means you can easily augment the behavior of applications without having to modify them directly.

The admin provides one feature that application developers often want to modify: managing users via the django.contrib.auth.models.User class. Often, the admin is the only place in which Django users are added or modified, making it difficult to customize this useful class.

Imagine you want the administrator of the site to receive an e-mail every time a new User object is created. Because the User model isn't directly available in the project, it might seem like the only way to accomplish this is to subclass User or use an indirect method such as creating a dummy profile object to modify.

Listing 11 demonstrates how easy it is to add a function that runs when a User instance is saved. Signals are usually added to models.py.

Listing 11. Using Django signals to notify when a new user is added

from django.db import models from django.db.models import signals from django.contrib.auth.models import User from django.core.mail import send_mail class Document(models.Model): [...] class Comment(models.Model): [...] def notify_admin(sender, instance, created, **kwargs): '''Notify the administrator that a new user has been added.''' if created: subject = 'New user created' message = 'User %s was added' % instance.username from_addr = 'no-reply@example.com' recipient_list = ('admin@example.com',) send_mail(subject, message, from_addr, recipient_list) signals.post_save.connect(notify_admin, sender=User)

The post_save signal is provided by Django and fires any time a model is saved or created. The connect() method here is taking two arguments: a callback ( notify_admin ) and the sender argument, which specifies that this callback is only interested in save events from the User model.

Inside the callback, the post_save signal passes the sender (the model class), the instance of that model, and a boolean ( created ) that indicates whether the instance was just created. In this example, the method sends an e-mail if the User is being created; otherwise it does nothing.

A list of other Django-provided signals is provided in Related topics, as well as documentation on how to write your own signals.

Deeper modifications: Adding row-level permissions

Why ? It might not be immediately obvious why a ForeignKey field would be set with blank=True when it isn't a text field. In this case, it's because the Django admin uses blank , rather than null , to determine whether the value must be manually set before saving the model. If you supply only null=True or neither, then the Django admin forces the user to manually select an "added by" value before saving, when instead you want the behavior to default to the current user on save.

A commonly requested feature of the Django admin is that its permission system be extended to include row-level permissions. By default, the admin allows for fine-grained control of roles and rights, but those roles apply only at the class level: a user can either modify all Documents or none.

Often, it is desirable to let users modify only specific objects. These are often called row-level permissions because they reflect the ability to modify only particular rows of a database table rather than blanket permission to modify any record in the table. A use case in the examples application might be that you want users to be able to see only Documents that they created.

First, update models.py to include an attribute recording who created the Document , as shown below.

Listing 12. Updating models.py to record the user who created each Document

from django.db import models from django.db.models import signals from django.contrib.auth.models import User from django.core.mail import send_mail class Document(models.Model): name = models.CharField(max_length=255) text = models.TextField() added_by = models.ForeignKey(User, null=True, blank=True) def get_absolute_url(self): return 'http://example.com/preview/document/%d/' % self.id def __unicode__(self): return self.name [...]

Next, you need to add code to automatically record which user created the Document . Signals don't work for this because the signal doesn't have access to the user object. However, the ModelAdmin class does provide a method that includes the request and, therefore, the current user as a parameter.

Modify the save_model() method in admin.py, as shown below.

Listing 13. Overriding a method in DocumentAdmin to save the current user to the database when created

from django.contrib import admin class DocumentAdmin(admin.ModelAdmin): def save_model(self, request, obj, form, change): if getattr(obj, 'added_by', None) is None: obj.added_by = request.user obj.last_modified_by = request.user obj.save() [...]

If the value of added_by is None , this is a new record that hasn't been saved. (You could also check if change is false , which indicates that the record is being added, but checking for whether added_by is empty means that this also populates records that have been added outside of the admin.)

The next piece of row-level permissions is to restrict the list of documents to only those users who created them. The ModelAdmin class provides a hook for this via a method called queryset() , which determines the default query set returned by any list page.

As shown in Listing 14, override queryset() to restrict the listing to only those Documents created by the current user. Superusers can see all documents.

Listing 14. Overriding the query set returned by the list pages

from django.contrib import admin from more_with_admin.examples import models class DocumentAdmin(admin.ModelAdmin): def queryset(self, request): qs = super(DocumentAdmin, self).queryset(request) # If super-user, show all comments if request.user.is_superuser: return qs return qs.filter(added_by=request.user) [...]

Now any requests for the Document list page in the admin show only those created by the current user (unless the current user is a superuser, in which case all documents are shown).

Of course, nothing currently prevents a determined user from accessing an edit page for an unauthorized document by knowing its ID. Truly secure row-level permissions require more method overriding. Because admin users are generally trusted to some degree anyway, sometimes basic permissions are enough to provide a streamlined workflow.

Conclusion

Customizing the Django admin does require some knowledge of the admin source code but little hacking. The admin is structured to be extensible using normal Python inheritance and some Django-only features such as signals.

The advantages of customizing the admin over creating a totally new administration interface are many:

Your application benefits from advances in Django as active development continues.

The admin already supports most common use cases.

External applications added to your project are automatically administrable side by side with your own code.

Looking ahead to Django V1.1, the admin provides two new features that are often requested: the ability to edit fields inline on the list pages and admin actions, which allow for bulk updates to many objects at once. Both additions will remove the need to write these common features from scratch while adding extension points for additional customization.

Downloadable resources

Related topics