Limiting Inline Admin Objects In Django

Dec. 21, 2008

6:16 pm

I'm now just over two months into my Django experiment and about halfway into my first client project. I'll admit to anyone who even feigns an interest in listening that I've never had this much fun working on the web. The framework never fails to impress.

On relating data.

If you're building a website or application of any considerable complexity, you're going to have one type of data that you want to relate with another. The example I seem to most often in CMS and programming tutorials is a database for musical acts. You'll have one-or-many artists who belong to none-or-many relate to bands. You can extend this example out to include, music videos, albums, individual songs – all distinct types of data that need to interrelate in your application or website.

ExpressionEngine, the CMS that powers this site, handles content relationships in a logical, user-friendly way that makes rapid development of data-heavy (and even data-heavy-esque) sites possible.

But EE does have its limits.

Take for example the problem I was confronted with in building an online registration system for an after-school program at a local elementary school. At its core, this app has five main data types: Courses, Sessions (that is, Fall 2009, Winter 2009, etc.), Course Instances (or a particular course happening during a particular session), Students, and Registrations – whose soul responsibility is to link a Student to a Course Instance. As parents register their children for courses, administrators will need to login to Django's admin interface and mark when the registration fee has been paid.

The problem.

Because the intermediary model does very little more than provide a bridge between two data types, I didn't want to require administrators to hunt down the appropriate registration instance to mark it as paid. Not that the admin site would have made this a particularly difficult task, but it just seems so much more natural to look up the student and have all current registrations listed on their admin change screen. If you're doing fifty of these in a shot and you've got a class to teach in fifteen minutes, you're gonna want the most logical system possible. Fortunately for me and the future administrators, inline model admin objects allow you to show child data inline with a parent model. Exactly what I needed.

Almost.

See, I got pretty ambitious with how I set up this data. What I really wanted to do was give the site admin's the ability to, at the end of each school year, mark a session as inactive and have all associated course instances effectively disappear from the site. So the Session data type has a field "is_active." Django makes it dead simple to limit course instances on the front end – and most of the backend – by providing a limit_choices_to parameter to foreign key fields (which are one of your options for relating one bit of data to another).

But when I set up my registration inline admins on student records, I noticed that the limit_choices_to option was being ignored. The docs do point this limitation out, but I'd completely missed it on first read.

limit_choices_to has no effect on the inline FormSets that are created to display related objects in the admin.

A hack-free solution.

Here I have to point out my hands-down most favorite thing about Django: hacks are almost never needed. That's not to say that the solution _solr from the Django irc channel helped me come up with isn't a bit inscrutable. But it involves absolutely no hacking of core files. It lives right in the admin module for your application. And, if you spend a bit of time studying Django's core, it actually does kinda start to make sense. Kinda.

  1. """
  2. Override the typical inline formset with a custom formset that
  3. excludes all registrations not related to active sessions.
  4.  
  5. """
  6. class StudentRegistrationInlineFormset(BaseInlineFormSet):
  7. def get_queryset(self):
  8. return super(StudentRegistrationInlineFormset, self).get_queryset().filter(course_instance__session__is_active=True)
  9.  
  10. class StudentRegistrationInline(admin.TabularInline):
  11. model = Registration
  12. fk_name = 'student'
  13. formset = StudentRegistrationInlineFormset
  14.  
  15. class StudentAdmin(admin.ModelAdmin):
  16. inlines = [
  17. StudentRegistrationInline
  18. ]

Now, if my boss asked, I'd have to admit that the solution is pretty much completely undocumented, and I did spend a good amount of time sussing it out. But the point is when all things are said and done, the solution is just so gosh durned beautiful. All we're doing is overriding the function – get_queryset() – that's responsible for providing data to a generic inline admin. The beauty is that we didn't have to do this for the whole system, or even for our whole project. This behavior is attached to just one inline admin in one app in our project. And nothing more.

I do so love clean code.

Comments

May 21, 2009

11:22 am

If I’m not mistaken, this doesn’t work any more (I’m using Django revision 10816 currently). Django tickets #11019, #10761 and #8071 might be related.

Antti Kaihola (#)

September 1, 2009

11:03 am

get_queryset() may now slice it before it returns, so that breaks this code. This is the way that I did it:

def get_queryset(self):
if not hasattr(self, ‘_queryset’):
if self.queryset is not None:
qs = self.queryset.filter(WHATEVER_I_WANT)
else:
qs = self.model._default_manager.get_query_set().filter(WHATEVER_I_WANT)
self.queryset = qs

return super(StudentRegistrationInlineFormset, self).get_queryset()

Leo Shklovskii (#)

September 14, 2009

7:15 am

Django tickets #11019, #10761 and #8071 might be related.

çet (#)

January 22, 2010

1:58 pm

www.vatanhaberleri.net
güncel haberler
vatan gazetesi
gazete oku
THANKS ADMİN GOOD Man (:

vatan gazetesi (#)

March 28, 2010

8:15 am

Admin Objects everytime good.

Bekir Cem (#)

March 31, 2010

11:34 pm

Beautiful expression. I like it, thanks! :)

True Blood (#)

May 16, 2010

8:41 am

Good article. I really like it. Admin objects is good.

Health (#)

Whaddya think?