Describing my MO for writing Admin sites is simple: quick, dirty but to the point and functional. In my mind, time spent hacking the Admin site is time that could have been spent improving the user-facing site.
But there are a number of things you can get for very little effort that really make working in that Admin site a lot nicer. It’s these little features that impress your clients.
Running jQuery (et al) scripts on an ModelAdmin Form
Today I needed to hide some optional fields unless a checkbox was selected. Unfortunately at the moment of writing, I don’t know a simple way of setting a dependency link between fields, least of all one that has an effect on the Admin frontend.
But there are a billion different reasons you might want a client-side script buzzing away on a form. Look how simple it is:
class FormAdmin(admin.ModelAdmin):
# other ModelAdmin options
# ...
class Media:
js = (
'/media/js/jquery-1.4.2.min.js',
'/media/js/admin-forms.js',
)
This loads jQuery and then your script where you actually do what you want. A simple example checks the checked
status of a checkbox and toggles the visibility of the <div>
holding the dependant field:
filters = function() {
$('.confirm_text').toggle(
$('#id_confirm_enabled').is(':checked')
);
};
$(function(){
filters();
$('#id_confirm_enabled').click(filters);
});
Magic.
Automatically generating slugs from titles (and checking for duplicates)
Again admin.ModelAdmin
steps in here and provides prepopulated_fields
. It’s a very simple JavaScript engine that creates simple slugs (omitting common words) when you create a new object (it doesn’t run on edits to keep the URL constant). Here’s it in action on this blog:
class PostAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("title",)}
One thing this doesn’t do (AFAIK) is do a duplicate-check. There are scenarios where two different titles could generate the same slug. This would result in a clash where the first object was found (or Model.objects.get(...)
would explode). To fix this we can improve our Model class to do a little duplicate check on-save. Here’s the relevant code from my BlogPost
class:
class BlogPost(models.Model):
slug = models.SlugField(max_length=100)
def save(self, *args, **kwargs):
while True:
others = BlogPost.objects.filter(slug=self.slug)
# If this instance is already saved we need to exclude it
if self.pk:
others = others.exclude(pk=self.pk)
if not others.count():
# We're good - we can save now
break
self.slug = '%s-' % self.slug
# Loops back to the beginning
super(BlogPost, self).save(*args, **kwargs)
Very simply this just keeps adding -
to the end of the slug until we hit an unused slug. Not super-elegant but you could customise this with numbers.
A common way of circumventing this whole debacle is just sticking the pk
in the URL in the first place. Speeds up lookup times too because you’re only doing an integer lookup in the database… But it looks uglier. Your choice ;)
Adding data to the Admin list view
Getting access to reporting is a fairly common request from Django newcomers. I’ve certainly had my fair share of issues. Going back to my Form
Model from the first part, each Form object represents a customisable form my clients can write and get their clients to fill in. Responses are written to another Model imaginatively called FormResponses
.
The response data isn’t much use to my clients so I wrote a CSV export view for them so they can grab the data and manhandle it in Excel… But how do they get this view? How do they filter down based on date? The forms are very date sensitive but are also reused. Time for some more ModelAdmin magic:
class FormAdmin(admin.ModelAdmin):
list_display = ('my other fields', 'available_data')
def available_data(self, form):
q = FormResponse.objects.filter(form_name=form.title)
if not q.count(): return 'No data yet...'
dates = q.dates('time', kind="month", order='ASC')
month_string = '<a href="/forms/download/%%s/%Y/%m/">%B %Y</a>'
months = ', '.join([d.strftime(month_string) % form.slug for d in dates])
return '<a href="/forms/get-data/%s/">All data</a>, %s' % (form.slug, months)
available_data.allow_tags = True
So what’s going on here? First I get a list of all the months that have data using the QuerySet.dates(field, kind, order)
function. This just throws back a list of datetimes. I need to turn those into sexy links and then prepend an “All data” link in case they want all the data at once.
list_display
pulls this function’s output into its own column in the list view and setting available_data.allow_tags
as True
means my code is regarded as safe and won’t be escaped. Here’s real version in action:
Customising the Admin headers
The default Django header isn’t terribly inspiring. It doesn’t help the user back to the site or help them skip between sections of the admin. It’s actually pretty useless. Thankfully we can plaster our own version over the top! Here’s one I made earlier for another client:
To do this all we need to do is create a replacement for the template admin/base_site.html
. Just create that file under your existing templates dir (create the admin/
dir if you don’t have it) and do something like the following:
{% extends "admin/base.html" %}
{% load i18n %}
{% block title %}{{ title }} | My New Title{% endblock %}
{% block branding %}
<h1 id="site-name">My new title for the Admin site!</h1>
{% endblock %}
{% block nav-global %}
{% if user.is_staff %}
<style type="text/css">
.ml {margin:0 10px 10px;display:block;float:left}
</style>
<a href="/" clas="ml">Website home</a>
<a href="/admin/" class="ml">Admin home</a>
<a href="/admin/members/invoice/" class="ml">Invoices</a>
<a href="/admin/auth/user/?is_active__exact=0" class="ml">New Users</a>
<a href="/admin/auth/user/" class="ml">All Users</a>
{% endif %}
{% endblock %}
This neatly brings me to the final thing for today:
Deep linking to filter models
As you can see from that last block of content, the third URL is looking pretty special:
/admin/auth/user/?is_active__exact=0
You’re able to make links directly to filtered querysets. This is handy if you want to give your clients access to, as I have here, a list of users who haven’t been vetted… Or comments that need moderating… Or anything else that isn’t the default state but is regularly used.
Did I miss your favourite feature? Let me know and I’ll add a sample. And if you’ve got any problems understanding my code, just ask. All this code is pulled (verbatim in some cases) from live projects so I’ll understand if you don’t get my hacktastrophes.