custom django admin
One of my favourite features of Django is the auto-created admin, if you’re like me and hate having to create UI and validate froms, then Django’s back office comes like a saviour from the heavens.

Unfortunately, sometimes it’s just necessary to have more control over how the admin looks and operates, the following tutorial will go through the (rather badly documented) ways you can edit and change the django admin to bend to your will.

Customising the admin templates

This is by far one of the easier aspects of customising django, as all the templates that django uses to publish the admin pages are standard templates, and can be overriden, or – more importantly – extended. It is my advice to you to extend the templates rather than completely override them, as this could save you an enourmous hastle later on when dealing with context variables and all the other hokey that django throws in.

To extend the templates – make an ‘admin’ folder in your templates directory, like so:

  • /admin/base.html – the base template, change headers, footers etc
  • /admin/login.html – the login page
  • /admin/index.html – the main menu the user sees
  • /admin/app_name/model_name/change_form.html – to edit the actual form pages for editing your models
  • /admin/css/ – To customise the CSS of the admin

You can copy all of the above from your django installation folder, they’ll be in contriib/admin/templates

A simple example

Showing two sets of interfaces for different users

I have an app where as an admin i don’tr really care about what the admin looks like, I just want access to the models – however, in some situations you’d like your users to have a nice, clean and polished admin with icons and a fixed layout, you can do something like this:

{% block content %}
<div id="content-main">

{% if app_list %}
    <!--- Check if the user is an admin or jsut plain old staff -->
    {% if user.is_superuser%}
        <!-- If they are kosher, show them the normal app list -->
    {% for app in app_list %}
        <div class="module">
        <table summary="{% blocktrans with app.name as name %}Models available in the {{ name }} application.{% endblocktrans %}">
        <caption><a href="{{ app.app_url }}" class="section">{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}</a></caption>
        {% for model in app.models %}
            <tr>
            {% if model.perms.change %}
                <th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
            {% else %}
                <th scope="row">{{ model.name }}</th>
            {% endif %}

            {% if model.perms.add %}
                <td><a href="{{ model.admin_url }}add/" class="addlink">{% trans 'Add' %}</a></td>
            {% else %}
                <td>&nbsp;</td>
            {% endif %}

            {% if model.perms.change %}
                <td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
            {% else %}
                <td>&nbsp;</td>
            {% endif %}
            </tr>
        {% endfor %}
        </table>
        </div>
    {% endfor %}
   
    {% else %}
    <!-- Otherwise, show them your custom menu! -->

    <div class="module">

    <!-- PUT YOUR OWN MENU IN HERE -->

    </div>
   
    {% endif %}
{% else %}
    <p>{% trans "You don't have permission to edit anything." %}</p>
{% endif %}
</div>
{% endblock %}

There’s not much to this – it’s all pretty straightforward – we check the user session variable to see if they are a superuser, and if they aren’t, then drop them into a customised menu structure.

Ok, what about changing the way item lists work?

Sometimes you will want to make sure that users only see their own content and not that of other users – e.g. in a blogging app where posts are hidden from one another – the first step here is to edit or create an admin.py file in your app and create a ModelAdmin class (I won’t discuss what this is here, but you can find lots of infor on it in the Django Documentation).

Here’s some code for blog entries that should only show the users content, and show all content to the admin:

class PostAdmin(admin.ModelAdmin):
   
    exclude = ('user',)

    def save_model(self, request, obj, form, change):
        if not change:
            obj.user = request.user
        obj.save()    
   
    def queryset(self, request):
        # Get the queryset form the parent class
        qs = super(PostAdmin, self).queryset(request)
        # Check for superuser
        if not request.user.is_superuser:
            # Not a superuser? Filter the queryset with the request object
            qs = qs.filter(user=request.user)
        return qs

You can see we’ve excluded a field, in this case user, as we don’t want people to be able to change the ownership of their posts. However, this is most likely a required field – so we need to override the save_model method to ensure it gets populated before committing the save.

You’ll notice the queryset() function, this is the heart of the issue – this function gets the queryset that displays in the list view of a model, as you can see it gets passed a request parameter, so we can do our checking here and simply filter the queryset before returning it to the app.

The final craziness – completely taking over an admin form and plugging it into Django

So, now you’ve decided you want a completely custom form for your models that works with your admin, this essentially works the same way as the above, just we’re subclassing a few extra functions:

class PostAdmin(admin.ModelAdmin):
   
    def change_view(self, request, storyid):
        # View for a change request
        return views.admin_change_view(request, self, storyid)
       
    def add_view(self, request):
        # View for an add request
        return views.admin_add_form(request, self)
   
    def get_urls(self):
        # Set up the URLS dynamically
        urls = super(PostAdmin, self).get_urls()
        my_urls = patterns('',
                           ('^(?P<storyid>d+)/$', self.change),
                           ('^add/$', self.add_view),
                        )
       
        return my_urls + urls

Next create your custom view:

def admin_change_view(request, model_admin, storyid=None):
   
    opts = model_admin.model._meta
    admin_site = model_admin.admin_site
    has_perm = request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
   
    # Here you do your own thing!
    obj = None
    if storyid:
        obj = story.objects.get(id=storyid)
   
    # Pass some objects back into the context for our custo form
    category = category.objects.all()
    galleries = gallery.objects.all()
   
    # Here you can add to the context - the marked items are new, the rest is
    # required by the admin site
    context = { 'admin_site': admin_site.name,
               'title': 'Add/Edit Story',
               'opts':opts,
               'editions': ed,          #New
               'galleries':galleries,   #New
               'obj':obj,               #New
               'root_path': '/%s' % admin_site.root_path,
               'app_label' : opts.app_label,
               'has_change_permission':has_perm}
    template = 'admin/story_change_form.html' # = Your new template
    return render_to_response(template, context,
                              context_instance=RequestContext(request))

Here’s what you’re doing:

Step 1

Update the ModelAdmin to include two new functions which will return your custom views to the URl parser and override the get_urls function to pass those views back into the admin to make sure your view gets called – if this doesn’t work you can always add the url manually to your urls.py

Step 2

Create your custom admin view – this can be anywhere, I put it in a seperate file called custom_admin.py and imported it in my main views.py, but you can do whatever you want.

This function should stay as is except the marked sections whcih are where the custom processing takes place.

The above example does not show a return value being saved or dealt with, but ideally you would pass a ModelForm through to the tremplate and then deal with it with a if request.methd == ‘POST’ to handle it.

An interesting thing about the admin forms is that the submit buttons return commands such as ‘_save’ and ‘_addanother’, you can catch these in your view to deal with the model and return value relatively easily.

That about covers it – there’s not a lot of information out there on how this is done well and the features are very badly documented in the Django admin – however it looks as if this is being addressed in future releases with a more coherent framework to actually customise the admin.

Happy coding,

Martin