Legacy BooleanField in Django

Mon 29 November 2010 by George Dorn

A legacy BooleanField supporting all kinds of antiquated ways of storing boolean values.

Django's inspectdb is pretty good at providing models that will at least read and write from your legacy database. But to get real power out of the ORM, you may need to provide some custom mapping for fields that don't behave exactly the way Django expects them to.

It's not terribly hard to subclass Django models and provide additional functionality. One thing that's missing from the BooleanField, for example, is support for legacy dbs that use really lame storage mechanisms like Y/N or 1/0, or even better, enum('True','False). The enum case is extra hilarious when you discover that mysql stores that as "True" = 0 and "False" = 1.

Here's a highly flexible legacy field:

from django.db import models

class LegacyBooleanField(models.BooleanField):
    __metaclass__ = models.SubfieldBase #need this for django to know to call to_python()

    def __init__(self, for_true='Y', for_false='N', db_type='char(1)', *args, **kwargs):
        super(LegacyBooleanField, self).__init__(*args, **kwargs)
        self.for_true = for_true
        self.for_false = for_false
        self._db_type = db_type

    def db_type(self, connection):
        return self._db_type

    def to_python(self, value):
        if value in (True, False):
            return bool(value)
        if value == self.for_true:
            return True
        if value == self.for_false:
            return False
        raise models.ImproperlyConfigured("LegacyBooleanField %s contained invalid element %s, not one of (%s, %s)" % (self.db_column, value, self.for_true, self.for_false))

    def get_prep_value(self, value):
        if value in (self.for_true, self.for_false):
            return value
        if value is True:
            return self.for_true
        if value is False:
            return self.for_false

    def get_prep_lookup(self, lookup_type, value):
        if value in ('1','0'): #special case for dealing with admin, see BooleanField.get_prep_lookup
            value = bool(value)
        if lookup_type == 'exact':
            return self.get_prep_value(value)
            raise TypeError('Lookup type %r not supported.' % lookup_type)

And now to use it, here's several examples:

from django.db import models
from legacy.fields import LegacyBooleanField

class MyModel(models.Model):
    yn_field = LegacyBooleanField(for_true='Y', for_false='N', db_column='is_yn')
    one_zero_field = LegacyBooleanField(for_true=1, for_false=0, db_type='tinyint', db_column='is_one_zero')
    enum_field = LegacyBooleanField(for_true='True', for_false='False', db_type='char(5)', db_column='my_lame_enum_field')

This allows you to use it like a proper python boolean, including in querysets:

obj = MyModel.objects.get(id=1234)
print obj.yn_field  #prints True or False
obj.yn_field = False
obj.save() #converts to 'N' behind the scenes
obj.yn_field = 'N'  #raises an error

objs = MyModels.objects.filter(enum_field=True).filter(one_zero_field=False)

(Update 12-15-2010: Forgot __metaclass__, which is vital.)