Dynamic upload paths in Django

25 August 2008

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:

import os
from django.db import models

def get_image_path(instance, filename):
    return os.path.join('photos', str(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.

Note: If you are using ModelForm, when you call form.save() it will save the file – no need to do it yourself as in the example above.

Update: Using the id of the instance doesn’t work any more because it’s probably not set when your function is called. Try using a field such as a slug instead, or the id of a parent object (e.g. if the Photo is in an Album, use the Album‘s id.

Filed under: Django — Scott @ 10:38 pm

31 Comments »

  1. Thanks for example Scott, there are really lots of new features since refactoring!

    Comment by ilya — 27 August 2008 @ 9:26 am

  2. 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

  3. @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

  4. 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

  5. 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

  6. 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

  7. For multiple models that modify the upload_to path you can use a single function to create the callable to pass to the upload_to parameter.

    def upload_to(path, attribute):
        
        def upload_callback(instance, filename):
            return '%s%s/%s' % (path, unicode(slugify(getattr(instance, attribute))), filename)
        
        return upload_callback
    
    ...
    #Model definitions
    class Photo(models.Model):
        name = models.CharField()
        image = models.ImageField(upload_to = upload_to('uploads/', 'name'))
    
    class Screenshot(models.Model):
        title = models.CharField()
        image = models.ImageField(upload_to = upload_to('uploads/', 'title'))

    Comment by bcline — 14 January 2009 @ 7:51 pm

  8. For completeness, the above code uses…

    from django.template.defaultfilters import slugify

    Comment by bcline — 15 January 2009 @ 9:25 am

  9. Needed this functionality, found your post, implemented it in 10 minutes. Super helpful, thanks!

    Comment by Tom — 20 May 2009 @ 5:30 am

  10. Yes, very helpful. Thanks a bunch!

    Comment by Moorthy — 25 May 2009 @ 11:18 am

  11. Really helpful, great!

    I have a question, is it possible to give the method a third argument?
    something like below?

    def get_image_path(instance, filename, number):
    return ‘photos/%s/%s%s’ % (instance.id, filename, number)

    and how would you use it in the model?

    thx

    Comment by Incinnerator — 3 August 2009 @ 10:02 pm

  12. @Incinnerator.

    The method gets called by Django, so you can’t add parameters to it. If you can work out the “number” from the instance, that’s best. Otherwise, I think you’ll need to get that number from the submitted form or from the view and set it separately on your model (Photo in my example).

    Comment by Scott — 4 August 2009 @ 12:22 pm

  13. Nice example, simple and to the point. However remember to use os.path.join instead of manually writing dir paths, or it wouldn’t work in (at least) Windows.

    import os
     
    def get_image_path(instance, filename):
        return os.path.join('photos', instance.id, filename)

    Comment by Fidel Ramos — 3 September 2009 @ 10:20 am

  14. Thanks, Fidel. I’ve updated the post to use os.path.join in the example.

    Comment by Scott — 3 September 2009 @ 10:56 am

  15. Thanks for the great example.

    Comment by Richard — 21 November 2009 @ 11:13 pm

  16. Thanks very much for this tutorial … it was a lifesaver for me. A small note to add to the comments:

    – The upload_to callable technique also works just fine if you’re uploading an image as part of larger form. You can save a form as normal in your view code, and the request.FILES data will be passed along to your model w/o problems, like this:

        form = UploadForm(request.POST, request.FILES)
        if form.is_valid():
            form.save() # all FILES data get passed along, get_image_path callable works fine
            return HttpResponseRedirect('/some/url/')

    Initially I got stuck on this point, because I was trying to save the form with form.save() AND save the image separately with the view code in your tutorial.

    Again thanks. There are a lot of outdated file/image upload tutorials on the web that can be quite confusing for django newcomers. This is great.

    Comment by Jesse — 2 January 2010 @ 6:24 pm

  17. hello thank you for the code, it helped a lot however I am having some issues..

    using django 1.2.1 and python 2.6
    first I had to add str wrapper around instance.id because id is of type long

    return os.path.join('folder', str(instance.id), filename)

    now the second issue only occurs when the model is first created:

                    pForm = ProjectForm(request.POST, request.FILES)
                    projInst = pForm.save(commit=False)
                    projInst.user = request.user
                    
                    projInst.status = 1
                    projInst.save()
    

    I get an error: ‘NoneType’ object has no attribute ‘startswith’, error occurs on the last line projInst.save()
    basicaly instance that is passed in to get_image_path is None

    however this works fine (editing a current object):

    pForm = ProjectForm(request.POST, request.FILES, instance=projInst)
    pForm.save()

    any idea why this is happening?
    thanks

    Comment by Bogdan — 20 September 2010 @ 4:41 am

  18. @Bogdan

    Using the id of the instance doesn’t really work, because it’s not guaranteed to exist when your upload path function is called. In this case it doesn’t because although the object has been created in memory, it has not yet been saved, so does not have an id.

    When you edit an existing object, the id is set so it does work.

    If you have another property, such as a slug field, which will be set, you can use that instead of the id. e.g.

    return os.path.join('folder', instance.slug, filename)

    I’ll update the code in the main post since it doesn’t seem to work any more.

    Comment by Scott — 20 September 2010 @ 9:32 am

  19. Why is there a ‘.’ after instance in your example:

    return os.path.join(‘photos’, instance., filename)

    I assume it is a typo as ‘syncdb’ complains when I have it…

    Comment by Davison — 10 November 2010 @ 11:48 pm

  20. I used the (fixed) version of the sample code and was able to add an image to my object via the Django admin page. However, when I click on the “Currently:” that the admin provides with the file pointer, it errors out as the path is not correctly written there.

    Specifically, the link from the admin page (for this object) gives this url:

    […]admin/interns/project/6/photos/ABC/photo.jpg

    However, the photo was uploaded into: /photos/ABC/photo.jpg

    Am I don’t something wrong, or should it somehow ‘just work’? Thanks for any help.

    – Davison

    Comment by Davison — 11 November 2010 @ 12:27 am

  21. @Davison

    The instance. should have been instance.id, fixed now, although using an id is problematic so it’s not the best example anyway.

    Comment by Scott — 18 November 2010 @ 12:57 pm

  22. Hello everyone,

    What do you think about this little hack :

    def get_image_path(instance, filename):
        id = instance.id
        if id == None:
            id = max(map(lambda a:a.id,Photo.objects.all())) + 1
        return os.path.join('photos', id, filename)

    So if the instance is not created, set the id to the most probable id ie the maximum current id of all the Photo + 1

    Comment by Eric — 8 February 2011 @ 9:15 pm

  23. @Eric

    I like it as a hack. Of course, if two people are adding at the same time, you could get the wrong id for one of them.

    Instead of iterating through the queryset, perhaps you could use aggregation something like Photo.objects.aggregate(Max('id'))['id__max']

    Comment by Scott — 9 February 2011 @ 10:41 am

  24. Thanks for this, really helpful.

    I needed to use a field from a different model – my image model had a foreign key on a category model and I wanted the URL slug for the category to be in the upload path. I tried this first:

    def get_media_path(instance, filename):
        return os.path.join('media', str(instance.media_type__type_slug), filename)
    
    This threw an error, but the syntax below (. instead of __) worked:
    
    def get_media_path(instance, filename):
        return os.path.join('media', str(instance.media_type.type_slug), filename)
    

    Just thought this might help someone.

    Comment by Matt Andrews — 15 May 2011 @ 1:31 pm

  25. You’ve noted that ‘instance.id’ will not work anymore. Is there a way to make it work? slugs would be nice, but in my case I have a field that stores a nickname. Since nicknames for trees are not unique, I can’t store the images for a tree in a distinct location if I used slugs. Since ids are unique, I was planning on using this scheme:

    mysite.com///pic.png

    where ‘id’ is the id of the particular Tree model that holds the ImageField and ‘slug’ is the Model’s title:

    class Tree(model.Models):
        def upload_image(self, filename):
             return 'uploads/%s/%s/%s' % (self.id, slugify(self.title), filename)
    
        title = models.CharField(......)
        pic = models.ImageField(upload_to='upload_image')
    

    As you already mentioned, ‘id’ won’t work anymore since id has not been created, so in the end I receive ‘None’ as the ‘id’. Is there a way around this?

    Comment by Mirror — 16 May 2011 @ 5:01 am

  26. Sorry, my example didn’t show up correctly. I mean to display this as the scheme:

    mysite.com/id/slug/pic.png

    Comment by Mirror — 16 May 2011 @ 5:03 am

  27. @Mirror

    In that situation I save the model instance first, then set the image. When the image is saved the model has an id so using it in the path is ok. This probably won’t work in admin though.

    Comment by Scott — 16 May 2011 @ 11:21 am

  28. Thanks, but can you further clarify on some confusion:

    How would I save the image after I save the Tree model? I have a model form like so:

    class TreeForm(forms.ModelForm):
       class Meta:
          model = TreeModel
          fields = ('title', 'pic')
    

    So in my views, I create a form by doing:

       if request == "POST":
          treeform = TreeForm(request.POST, request.FILES)
          ....
    
          if treeform.is_valid():
             new_tree = treeform.save()
             ....
    

    Here, saving the pic (through upload_to) will be bundled with saving the entire TreeModel, so id will be ‘None’ at this point. How would you separately save the picture if the pic is a field bound to the TreeModel you’re already saving? Because when the user clicks ‘save’ on the form, it’s going to run through saving the pic as it saves the other fields in TreeModel.

    One solution would be to use cleaned_data:

         pic = treeform.cleaned_data['pic']
         new_tree = treeform.save(commit=False)          <=== to ensure that I don't save the image yet because this will produce id=None
         new_tree.pic = pic
         new_tree.save()           <== id shouldn't be None here, I think
    

    second solution would be to modify how save() works in TreeModel, but I'm not sure how that would work.

    Comment by Mirror — 16 May 2011 @ 5:29 pm

  29. Actually, I meant to say that I came up with those solutions and am not sure if they are correct solutions rather than just ugly hacks.

    Comment by Mirror — 16 May 2011 @ 5:32 pm

  30. @Mirror

    I think if you save with commit=False, you don’t get an id, in which case that won’t help.

    It’s not as nice as just saving the form, but the way to do it would be pass just request.POST (not request.FILES) to the form, then save the file from request.FILES manually.

    That’s the gist of it. Sorry, I don’t have any code to share – when I did it the situation was different.

    Comment by Scott — 16 May 2011 @ 8:40 pm

  31. Thanks for this sample, worked perfectly.

    Comment by JayKay — 18 February 2012 @ 10:35 pm

RSS feed for comments on this post.

Leave a comment