Dynamic upload paths in Django
For a while I’ve been using the CustomImageField as a way to specify an upload path for images. It’s a hack that lets you use ids or slugs from your models in the upload path, e.g.:
/path/to/media/photos/1234/flowers.jpg
or
/path/to/media/photos/scotland-trip/castle.jpg
CustomImageField no more
Since the FileStorageRefactor was merged in to trunk r8244, it’s no longer necessary to use the custom field. Other recent changes to trunk mean that it doesn’t work any more in its current state, so this is a good time to retire it.
Pass a callable in upload_to
It is now possible for the upload_to parameter of the FileField or ImageField to be a callable, instead of a string. The callable is passed the current model instance and uploaded file name and must return a path. That sounds ideal.
Here’s an example:
from django.db import models
def get_image_path(instance, filename):
return 'photos/%s/%s' % (instance.id, filename)
class Photo(models.Model):
image = models.ImageField(upload_to=get_image_path)
get_image_path is the callable (in this case a function). It simply gets the id from the instance of Photo and uses that in the upload path. Images will be uploaded to paths like:
photos/1/kitty.jpg
You can use whatever fields are in the instance (slugs, etc), or fields in related models. For example, if Photo models are associated with an Album model, the upload path for a Photo could include the Album slug.
Note that if you are using the id, you need to make sure the model instance was saved before you upload the file. Otherwise, the id hasn’t been set at that point and can’t be used.
For reference, here’s what the main part of the view might look like:
...
if request.method == 'POST':
form = PhotoForm(request.POST, request.FILES)
if form.is_valid():
photo = Photo.objects.create()
image_file = request.FILES['image']
photo.image.save(image_file.name, image_file)
...
This is much simpler than the hacks used in CustomImageField and provides a nice flexible way to specify file or image upload paths per-model instance.
Thanks for example Scott, there are really lots of new features since refactoring!
Comment by ilya — 27 August 2008 @ 9:26 am
For some reason, whenever one posts a new image (I am doing this from the admin interface) using the above method, instance.id returns None, which then gets inserted in my upload_to path where I thought instance.id should have gone… Do you have any recommendations as to what I should do? I’ve been playing with this a lot, and I’ve had no luck. Thank you very much!
Comment by Kevin Holzer — 31 August 2008 @ 9:49 am
@Kevin Holzer
I don’t see a good way around that, Kevin. Admin is getting all the field values (including the upload_to path) before saving. Before the first save, there is no id for you to use. In your own views, you get to create the model instance first and then save the image, but not with admin.
If there’s another field you can use, such as a slug, that’s probably the best alternative. Otherwise, you could upload to a temporary path and respond to the post_save signal by moving the image to its proper location.
Comment by Scott — 1 September 2008 @ 9:18 am
Thanks alot! I’ve a bunch of these CustomImageField hacks, and I’m just starting to update them, but first needed to get up-to-date on the refactoring stuff. You’ve really made it easier for me.
Comment by Bjorn — 2 September 2008 @ 1:32 pm
NOTE (OBS!): If you are using this method with a ModelForm, and using data from the fields for your filename, your image field MUST come after (in the model) the other fields you want to access in creating the filename. The fields in the instance which is passed into your callable are filled out in order of declaration, so if your image is defined first, the instance will have EMPTY STRINGS for all other fields and you will get an EMPTY FILENAME.
Comment by Kellen — 15 September 2008 @ 5:57 pm
Thanks a lot for both the code and the decent guide-through. Nice, simple code and works perfectly.
Comment by Bulak — 27 December 2008 @ 4:15 pm