views:

190

answers:

3

We implemented a LowerCaseCharField. We would be happy to hear better implementation suggestions.

from django.db.models.fields import CharField

class LowerCaseCharField(CharField):
    """
    Defines a charfield which automatically converts all inputs to
    lowercase and saves.
    """

    def pre_save(self, model_instance, add):
        """
        Converts the string to lowercase before saving.
        """
        current_value = getattr(model_instance, self.attname)
        setattr(model_instance, self.attname, current_value.lower())
        return getattr(model_instance, self.attname)

In fact we love to have is:

> modelinstance.field_name="TEST"
> print modelinstance.field_name
'test'

current implementation only converts to lowercase when it is saved.

+1  A: 

You try making field_name a property and do whatever in getter or setter.

Kugel
+1: It's not a different type of field, it's a constraint on an existing type.
S.Lott
The point is that we have this *constraint* on 12 fields on 11 different models so this is the solution we found to obey "DRY".
Can Burak Cilingir
+3  A: 

You may wish to override to_python, which will allow you to compare non-lowercase strings when doing database lookups. The actual method is get_prep_value, but as that calls to_python for CharField, it's more convenient to override that:

def to_python(self, value):
    value = super(LowerCaseCharField, self).to_python(value)
    if isinstance(value, basestring):
        return value.lower()
    return value

Now you can do queries like:

MyModel.objects.filter(lccf="MiXeD")

Edit:

Rereading your question, it looks like you want the lowering to take effect immediately. To do this, you'll need to create a descriptor (a new-style python object with __get__ and __set__ methods, see the python docs and the django code for related models) and override contribute_to_class in the field to set the model's field to your descriptor.

Here is a full example off the top of my head, which should be reusable for all fields that want to modify the value on setting.

class ModifyingFieldDescriptor(object):
    """ Modifies a field when set using the field's (overriden) .to_python() method. """
    def __init__(self, field):  
        self.field = field  
    def __get__(self, instance, owner=None):
        if instance is None:
            raise AttributeError('Can only be accessed via an instance.')  
        return instance.__dict__[self.field.name]
    def __set__(self, instance, value):
        instance.__dict__[self.field.name] = self.field.to_python(value)

class LowerCaseCharField(CharField):
    def to_python(self, value):
        value = super(LowerCaseCharField, self).to_python(value)
        if isinstance(value, basestring):
            return value.lower()
        return value
    def contribute_to_class(self, cls, name):
        super(LowerCaseCharField, self).contribute_to_class(cls, name)
        setattr(cls, self.name, ModifyingFieldDescriptor(self))
Will Hardy
The need for this surfaced when we are storing email addresses, so in fact, to_python by itself would suffice to solve our problem.Now, the discussion is about normalizing the data in the database by converting it to lowercase is logical or not.The rfc, quoted on http://en.wikipedia.org/wiki/E-mail_address#RFC_specification says it is *discouraged* to distinguish between uppercase and lowercase email addresses.The path I would like to take is,1. Normalize it by converting to lowercase2. Ignore the case while querying as the user input will be case insensitive.
Can Burak Cilingir
The above code should do both: 1. the string is normalised the moment the attribute is set (`ModifyingFieldDescriptor`) 2. all strings used in queries are normalised, so they are by default case insensitive (`to_python`). Normalising is a good approach, because you will never need the original, case-sensitive value and it will enable you to have database driven unique checks etc.
Will Hardy
Your approach seems to be the right way to do this. This is the first time we need to use contribute_to_class, so this is a new adventure. Thanks for your time and *in fact* also promotiong stackoverflow to me.
Can Burak Cilingir
A: 

Using Python properties is not straightforward because of the way django.models.Models magically set their instance attributes based on class attributes. There is an open bug out for this.

I ended up doing exactly what the original poster did.

It means you have to do:

>>> modelinstance.field_name="TEST"
>>> modelinstance.save() # Extra step
>>> print modelinstance.field_name
'test'
blokeley