I've got this model in Django:
class MyModel(models.Model):
completed_date = models.DateTimeField(null=True)
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.