Undocumented Django: Overriding _get_FIELD_url and friends

Recently I found the need to override the _get_FIELD_url() for a particular set of models in my Django app. The _get_FIELD_*() methods provide your FileField and ImageField with “magic” convenience methods. For example:

from django.db import models

class MyModel(models.Model):
    file = models.FileField(upload_to=’some/path’)

If you want to to retrieve the URL for the file you would use:

m = MyModel().objects.get(id=1)
m.get_file_url()

Notice that your class automagically gets a method called “get_file_url()”. This is the “magic” part and all of this is done behind the scenes for you. While this is really convenient for getting things done most of the time, there are some times where you need to have more control over what’s returned by these magic methods.

You’re not in OOP Kansas anymore…

All of the _get_FIELD_ methods are defined in the Model class. Normally when you want to replace logic for a method in a subclass, you would override the method in the subclass. So the most straightforward approach would be to create a subclass of Model and override the _get_FIELD_ methods you need. Then you would use the subclass for your own models where you need _get_FIELD_* overridden. This approach does not work (try it if you don’t take my word for it). It will take too long to explain why this won’t work but suffice to say, the current implementation does not allow for subclassing a subclass of Model. The Django team is working on getting around this limitation.

Why would you want to replace them in the first place? In my app, I had the need for “protected” and “unprotected” downloads. I needed the URL’s for “protected” files to be rooted to /protected and “unprotected” files to /media This meant that I had to use a different MEDIA_ROOT and MEDIA_URL for the “protected” files and a different one for “unprotected” files. Because of the way things are set up by default in Django, you can only have one MEDIA_ROOT and MEDIA_URL for all your models. Plus having to embed the URL root in your templates would be tedious to maintain.

So what do you do when you want to replace the logic for _get_FIELD_? Since subclassing won’t work, what other options do you have? One solution, and the one I used for my app, is to use mix-ins. We still won’t be able to override the existing _get_FIELD_ methods in the Model class but using a ProtectedDownloadModelMixIn we will be able to provide alternate implementations under different names. For instance, we only need to override the _get_FIELD_url() method or provide a suitable replacement for it in the mix-in. Let’s call the new method, _get_PROTECTED_FIELD_url(). This is straight out of the django/db/models/base.py module with a few modifications.

class ProtectedDownloadModelMixIn(object):
    def _get_PROTECTED_FIELD_url(self, field):
        if getattr(self, field.attname): # value is not blank
            import urlparse
            return urlparse.urljoin(settings.PROTECTED_MEDIA_URL, getattr(self, field.attname)).replace(’\\’, ‘/’)
        return ”

The _get_FIELD_* methods are automatically added to the fields through the contribute_to_class() method of the field. To make our model use our custom _get_PROTECTED_FIELD_url() we need to create a subclass of either a FileField or ImageField and override the contribute_to_class method. In my app, I needed to subclass the FileField class.

from django.utils.functional import curry
from django.db import get_creation_module

class ProtectedFileField(models.FileField):
    def contribute_to_class(self, cls, name):
        super(ProtectedFileField, self).contribute_to_class(cls, name)
        setattr(cls, ‘get_%s_url’ % self.name, curry(cls._get_PROTECTED_FIELD_url, field=self))

# Register our new field.
data_types = get_creation_module().DATA_TYPES
data_types['ProtectedFileField'] = data_types['FileField']

The last bit will register our new field with Django so that the database backend will know what SQL to use when the field is created. Now to use our custom classes:

from django.db import models

class ProtectedFile(models.Model, ProtectedDownloadModelMixIn):
    file = ProtectedFileField(upload_to=’protected/’
    mime = models.CharField(max_length=64)

That’s pretty much it! Now if you need to override the other _get_FIELD_*, for instance you might want to override _save_FIELD_file() you need to define that method in the mix-in and add the method to the class in contribute_to_class. There are a lot of things that I have left unexplained here. This is because most of this is “magic” and Django is going through “magic removal” so all of this will become irrelevant eventually. If you really need to understand what’s going on, try looking at the code in django/db/models/base.py and in django/db/fields/init.py.

Leave a Reply

Comments are moderated by the administrator. If this is your first time posting a comment, your comment will go to a moderation queue and it may take a while for your comment to appear. Or it may get deleted.