The Django Admin

Django comes with:

  • Persistence via the Django ORM
  • URL Mapping via urlpatterns
  • Templating via the Django Template Language
  • Caching with levels of configurability
  • Internationalization via i18n hooks
  • Form rendering and handling
  • User authentication and authorization

Pretty much everything you need to make a solid website quickly.

Lots of frameworks offer some of these features, if not all.

What is Django’s killer feature

The Django Admin

The Django Admin works in concert with the Django ORM to provide automatic CRUD functionality. You write the models, it provides the UI.

The Django Admin is a great example of the Pareto Priciple, a.k.a. the 80/20 rule:

80% of the problems can be solved by 20% of the effort

The converse also holds true:

Fixing the last 20% of the problems will take the remaining 80% of the effort.

Using the Django Admin

The Django Admin is, itself, an app, installed by default (as of 1.6).

In earlier versions of Django, you might need to install it. This is accomplished by adding it to the INSTALLED_APPS setting:

INSTALLED_APPS = (
    'django.contrib.admin', # <- make sure it's there and uncommented
    # ...
)

What we need now is to allow the admin to be seen through a web browser.

To do that, you have to add URLs to your project.

Django URL Resolution

You’ve seen how Flask resolves URLs using “routes”. Django too has a system for dispatching requests to code: the urlconf.

  • A urlconf is an iterable of calls to the django.conf.urls.url function
  • This function takes:
    • a regexp rule, representing the URL
    • a callable to be invoked (or a name identifying one)
    • an optional name kwarg, used to reverse the URL
    • other optional arguments we will skip for now
  • The function returns a resolver that matches the request path to the callable

urlpatterns

I said above that a urlconf is an iterable. That iterable is generally built by calling the django.conf.urls.patterns function. It’s best to build it that way, but in reality, any iterable will do.

However, the name you give this iterable is not flexible.

Django builds a list of available urls by loading the urlconf named urlpatterns that it finds in the file named in settings.ROOT_URLCONF.

You can build a urlconf by adding urls one at a time. However many Django add-on apps, like the Django Admin, come with their own urlconf. For apps like this, it is standard to include the urls defind by these apps by rooting them at some path in your site.

You can do this by using the django.conf.urls.include function as the callable in a url call:

# ...
url(r'^forum/', include('random.forum.app.urls')),
# ...

We can use this to add all the URLs provided by the Django admin in one stroke. If you open the urls.py file in a project made by startproject you should find the following lines are already present:

from django.contrib import admin #<- make sure these two are
admin.autodiscover()             #<- present and uncommented

urlpatterns = patterns('',
    ...
    url(r'^admin/', include(admin.site.urls)), #<- and this
)

We can now view the admin. We’ll use the Django development server.

Viewing the Admin

In your terminal, use the runserver management command to start the development server:

(djangoenv)$ python manage.py runserver
Validating models...

0 errors found
Django version 1.4.3, using settings 'mysite.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Load http://localhost:8000/admin/. You should see this:

../../_images/django-admin-login.png

You can log in with the name and password you created when you first ran syncdb for your project. If you can’t remember that, you can always use the createsuperuser command to add a new superuser and use that one to log in.

After logging in, you’ll be viewing the Django Admin index. The index page will have a visible section for each installed app that provides one or more Models registered with the admin. Each of these sections will provide a list of models registered. You should see something like this:

../../_images/admin_index.png

Click on Users. Find yourself? Edit yourself, but don’t uncheck superuser.

Adding Models to the Admin

In order to use the Admin to interact with your models, you must register them. Django expects these registrations to take place in a file called admin.py.

Find that file in your app and add code to register your model(s):

from django.contrib import admin
from your_app.models import SomeModel

admin.site.register(SomeModel)

When a model is registered with the Django Admin, new urls for interacting with it are automatically generated by the Django Admin urlconf. Because we’ve used django.conf.urls.include to add all the admin urls to our project urlconf, there’s no further work needed.

After registering your models, simple reload the admin index page. You should see a section for your app listed, and all your registered models listed in that section.

Customizing the Admin

While the admin is quite nice in terms of what it gives you. It is built to fulfill that Pareto Principle. There are often modifications you need to make in order to get things just right.

For example, consider a blogging app like the one we created in Flask. In such a system, a Post can be categorized. Where the Django Admin displays the UI for this will be dependent on which model holds the ManyToManyRelationship field for that connection.

If you place that field on the Category object then you end up in a situation where you need to add Posts to Categories, rather than the more natural workflow of choosing Categories for Posts.

https://docs.djangoproject.com/en/1.6/ref/contrib/admin/

The ModelAdmin Class

The key to customizing how a model is administered by the Django Admin is to create a customized sublcass of the django.contrib.admin.ModelAdmin class.

Looking at the admin.py file generated for a Django app, you’ll see a few things of interest.

  • The admin.site with which you register classes is a globally available instance of the Admin class.
  • It is initialized at runtime automatically.
  • It stores a registry of the models that are registered with it.
  • Each call to admin.site.register adds a new model to the global site.
  • register takes two args: a Model subclass and an optional ModelAdmin subclass
  • If you call it without the optional subclass, you get the default.

Most usable admin functions and attributes are provided by the ModelAdmin class.

For example, you can influence the tabular display of fields from your Model class by using the list_display attribute:

class PostAdmin(admin.ModelAdmin):
    list_display = ('__unicode__', 'created_date', 'modified_date',
                    'published_date', 'author')

admin.site.register(Post, PostAdmin)

By adding or omitting fields from this tuple, you control what appears on the table listing all existing Post objects in your database.

Pretty simple, eh?

A Couple of things about the list_display option are important to know:

  • The value you provide must be an iterable even if it has only one item
  • Each item in the iterable becomes a column in the list
  • The first item is the one that links to the change page for that object
    • That can be customized by the list_display_links option
  • Listed items can be field names or callables.
  • Callables can be module-level functions, or methods on the ModelAdmin or Model

As an example, you could use this callable to provide a link to the admin page for the User who is the author of a post, instead of just the user’s id.

Your functionality might provide:

  • The full name of the author, if present, otherwise the username.
  • A link to the admin change form for that author.

But where to add this code? The admin.py Module? On your custom ModelAdmin? On the Post Model?

Let’s think about each of our two functionalities.

  • The first could be useful in public listings
  • The second is really only useful on the admin.

This would tend to point to providing the first as a method on the Post model, and the second as a model admin callable.

The first might look like this:

class Post(models.Model):
    # ...
    def author_name(self):
        raw_name = "%s %s" % (self.author.first_name,
                              self.author.last_name)
        name = raw_name.strip()
        if not name:
            name = self.author.username
        return name

The second might appear this way in your custom ModelAdmin class in admin.py:

# add an import
from django.core.urlresolvers import reverse

# and a method
class PostAdmin(admin.ModelAdmin):
    #...
    def author_link(self, post):
        url = reverse('admin:auth_user_change', args=(post.id,))
        name = post.author_name()
        return '<a href="%s">%s</a>' % (url, name)

Notice that the author_link function we just wrote returns HTML. Like any good framework, Django will escape this HTML before displaying it. You’ll also need to let the admin know your HTML is safe:

def author_link(self, post):
    #... method body
author_link.allow_tags = True #<- see that bit there?

In Python, everything is an object. Even methods of classes.

The Django admin uses special method attributes to control the methods you create for list_display.

Another special attribute controls the column title used in the list page:

def author_link(self, post):
    #... method body
author_link.allow_tags = True
author_link.short_description = "Author" #<- add this

Finally, replace the 'author' name in list_display with 'author_link':

list_display = (..., 'author_link')

Testing Admin Changes

Of course, any changes or customizations we make to default Admin behavior should be covered by tests. To test the admin, you first need a new TestClass:

# new imports
from django.contrib.admin.sites import AdminSite
from myapp.admin import PostAdmin

# new TestCase
class PostAdminTestCase(TestCase):
    fixtures = ['some_initial_data.json', ]

    def setUp(self):
        admin = AdminSite()
        self.ma = PostAdmin(Post, admin)
        for author in User.objects.all():
            title = "%s's title" % author.username
            post = Post(title=title, author=author)
            post.save()

The key is instantiating both an AdminSite and a ModelAdmin to serve in your tests. Once that’s set, you can test methods of that ModelAdmin:

def test_author_link(self):
    expected_link_path = '/admin/auth/user/%s'
    for post in Post.objects.all():
        expected = expected_link_path % post.author.pk
        actual = self.ma.author_link(post) # <- Using the admin here
        self.assertTrue(expected in actual)

First, replace the 'author' name in list_display with 'author_link':

list_display = (..., 'author_link')

We also need to let the admin know our HTML is safe:

def author_link(self, post):
    #... method body
author_link.allow_tags = True

Inline Admins

It would also be nice to add Categories to Posts when editing the latter, rather than having to do it the other way around.

But there is no field on the Post model that would show Categories.=

Django provides the concept of an inline form to allow adding objects that are related when there is no field available.

In the Django Admin, these are created using subclasses of the InlineAdmin.

For example, an inline admin to edit Categories in the context of a Post might look like this:

class CategoryInlineAdmin(admin.TabularInline):
    model = Category.posts.through
    extra = 1

Once that is in place, all you need is one line added to the PostAdmin class definition:

class PostAdmin(admin.ModelAdmin):
    #... other options
    inlines = [CategoryInlineAdmin, ]

    #... methods

The key is to define the inline before the model admin where you will use it so that the class is available when you reference it. Otherwise you’ll get NameErrors.

It’s even possible to remove the ‘posts’ field from the Category object, so that if you are editing a category you can avoid having to see the posts to which it is related:

# create a custom model admin class
class CategoryAdmin(admin.ModelAdmin):
    exclude = ('posts', )

# and register Category to use it in the Admin
admin.site.register(Category, CategoryAdmin)

There’s a great deal more to learn about the Django admin. I encourage you to read the documentation and explore ways you can make the admin perform the way you’d like it to.