My dislike for booleans and that impact on the Django Admin


1st of June 2009

I've got this model in Django:

 class MyModel(models.Model):
    completed_date = models.DateTimeField(null=True)

My dislike for booleans and that impact on the Django Admin By using a DateTimeField instead of a BooleanField I'm able to record if an instance is completed or not and when it was completed. A very common pattern in relational applications. Booleans are brief but often insufficient. (Check out Ned Batchelder's Booleans suck)

To make it a bit more convenient (and readable) to work with I added this method:

 class MyModel(models.Model):
    completed_date = models.DateTimeField(null=True)

    @property
    def completed(self):
        return self.completed_date is not None

That's great! Now I can do this (use your imagination now):

 >>> from myapp.models import MyModel
 >>> instance = MyModel.objects.all()[0]
 >>> instance.completed
 False
 >>> instance.completed_date = datetime.datetime.now()
 >>> instance.save()
 >>> instance.completed
 True

I guess I could add a setter too.

But Django's QuerySet machinery doesn't really tie in with the ORM Python classes until the last step so you can't use these property methods in your filtering/excluding. What I want to do is to be able to do this:

 >>> from myapp.models import MyModel
 >>> completed_instances = MyModel.objects.filter(completed=True)
 >>> incomplete_instances = MyModel.objects.filter(completed=False)

To be able to do that I had to add special manager which is sensitive to the parameters it gets and changes them on the fly. So, the manager plus model now looks like this:

 class SpecialManager(models.Manager):
    """turn certain booleanesque parameters into date parameters"""

    def filter(self, *args, **kwargs):
        self.__transform_kwargs(kwargs)
        return super(SpecialManager, self).filter(*args, **kwargs)

    def exclude(self, *args, **kwargs):
        self.__transform_kwargs(kwargs)
        return super(SpecialManager, self).exclude(*args, **kwargs)

    def __transform_kwargs(self, kwargs):
        bool_name, date_name = 'completed', 'completed_date'
        for key, value in kwargs.items():
            if bool_name == key or key.startswith('%s__' % bool_name):
                if kwargs.pop(key):
                    kwargs['%s__lte' % date_name] = datetime.now()
                else:
                    kwargs[date_name] = None

 class MyModel(models.Model):
    completed_date = models.DateTimeField(null=True)

    @property
    def completed(self):
        return self.completed_date is not None

Now, that's fine but there's one problem. For the application in hand, we're relying on the admin interface a lot. Because of the handy @property decorator I set on the method completed() I now can't include completed into the admin's list_display so I have to do this special trick:

 class MyModelAdmin(admin.ModelAdmin):
    list_display = ('is_completed',)

    def is_completed(self, object_):
        return object_.completed
    is_completed.short_description = u'Completed?'
    is_completed.boolean = True

Now, I get the same nice effect in the admin view where this appears as a boolean. The information is still there about when it was completed if I need to extract that for other bits and pieces such as an advanced view or auditing. Pleased!

Now one last challenge with the Django admin interface was how to filter on these non-database-fields? It's been deliberately done so that you can't filter on methods but it's slowly changing and with some hope it'll be in Django 1.2. But I'm not interested in making my application depend on a patch to django.contrib but I really want to filter in the admin. We've already added some custom links and widgets to the admin interface.

After a lot of poking around and hacking together with my colleague Bruno Renié we came up with the following solution:

 class MyModelAdmin(admin.ModelAdmin):
    list_display = ('is_completed',)

    def is_completed(self, object_):
        return object_.completed
    is_arrived.short_description = u'Completed?'
    is_arrived.boolean = True

    def changelist_view(self, request, extra_context=None, **kwargs):
        from django.contrib.admin.views.main import ChangeList
        cl = ChangeList(request, self.model, list(self.list_display),
                        self.list_display_links, self.list_filter,
                        self.date_hierarchy, self.search_fields, 
                        self.list_select_related,
                        self.list_per_page,
                        self.list_editable, self)
        cl.formset = None

        if extra_context is None:
            extra_context = {}

        if kwargs.get('only_completed'):
            cl.result_list = cl.result_list.exclude(completed_date=None)
            extra_context['extra_filter'] = "Only completed ones"

        extra_context['cl'] = cl
        return super(SendinRequestAdmin, self).\
          changelist_view(request, extra_context=extra_context)

    def get_urls(self):
        from django.conf.urls.defaults import patterns, url
        urls = super(SendinRequestAdmin, self).get_urls()
        my_urls = patterns('',
                url(r'^only-completed/$', 
                    self.admin_site.admin_view(self.changelist_view),
                     {'only_completed':True}, name="changelist_view"),
        )
        return my_urls + urls

Granted, we're not getting the nice filter widget on the right hand side in the admin interface this time but it's good enough for me to be able to make a special link to /admin/myapp/mymodel/only-completed/ and it works just like a normal filter.

Ticket 5833 is quite busy and has been going on for a while. It feels a daunting task to dig in and contribute when so many people are already ahead of me. By writing this blog entry hopefully it will help other people who're hacking on their Django admin interfaces who, like me, hate booleans.



Comment

Show all 8 comments
 
Name:
Email:
hide my email address.

Your email address will be encoded to prevent email-extraction spiders from reading it so you won't get spammed if you decide to show your email address.