Uploading images to a dynamic path with Django
Django makes it easy to upload images by adding an
ImageField to your model. The images are uploaded to your media path in a subdirectory specified with the
upload_to parameter which can contain a date/time pattern like
class Photo(models.Model): caption = models.CharField(blank=True, maxlength=100) image = models.ImageField(upload_to='photos/%Y/%m/%d')
In this example, images will be uploaded to a path like:
Sometimes you want to keep related images together, rather than spreading them over multiple date directories. But if you have a lot of images, you won’t want them all stored in a single directory.
It would be nice if there was a way to upload to a directory specific to the model, perhaps a path incorporating the model object’s id or a unique slug. Something like:
Django doesn’t have a standard way to do this at the moment (it’s pending a design decision according to ticket #4113).
I needed to do this in a project recently and tried various different approaches. Here’s what I tried – skip to the working solution if you’re not interested in the failed attempts.
Attempt 1: Specify
Why not make
upload_to include the id of the model?
class Photo(models.Model): caption = models.CharField(blank=True, maxlength=100) image = models.ImageField(upload_to='photos/%d' % self.id)
Because there is no
self; that’s why. Django builds the model with the fields we specify, but at that point, there is no instance of the model, so
self is meaningless. Whatever we put in
upload_to here will apply to all instances of the model.
Attempt 2: Set
upload_to on save
How about overriding the
save method of the model and setting
upload_to on the image field at that point. Something like:
class Photo(models.Model): caption = models.CharField(blank=True, maxlength=100) image = models.ImageField(upload_to='photos') def save(self): for field in self._meta.fields: if field.name == 'image': field.upload_to = 'photos/%d' % self.id super(Photo, self).save()
It’s a bit icky having to iterate through
self._meta.fields to find the right one. But a bigger problem is the image file may well be written to the path before
save is called.
Usually you would call
photo.save_image_file(filename, content) to save the file content then
photo.save() to save the model’s fields, including the path in the image field. By the time we set
upload_to, it’s too late.
Attempt 3: Override
ImageField and pass a callable for
Taking a different approach, how about making a new class that derives from ImageField and takes either a new parameter or a callable for the
upload_to parameter. Something like:
class SpecialImageField(ImageField): def get_directory_name(self): if callable(self.upload_to): return self.upload_to() else: return super(SpecialImageField, self).get_directory_name()
We override the
get_directory_name method which is actually defined in
FileField (from which
The problem is, we’re still having to pass something (a callable) for the
upload_to parameter. Again, at the time the
ImageField is created, we are not in an instance of the model, so we can’t pass a it a model method. We could pass a module-level function, but that’s not enough information; we want to set the path using something in the model instance.
Attempt 4 – the one that worked: Override
ImageField get model instance and ask it
After going round in circles and learning a few things on the way, I came across this page in the Django wiki (mental note: check wiki first in future).
It shows how a custom field can get hold of the model instance using dispatcher. I wrote
CustomImageField to get the model instance when the model instance is initialised and ask it to supply a new
The field looks like this:
from django.db.models import ImageField, signals from django.dispatch import dispatcher class CustomImageField(ImageField): """Allows model instance to specify upload_to dynamically. Model class should have a method like: def get_upload_to(self, attname): return 'path/to/%d' % self.id Based on: http://code.djangoproject.com/wiki/CustomUploadAndFilters """ def contribute_to_class(self, cls, name): """Hook up events so we can access the instance.""" super(CustomImageField, self).contribute_to_class(cls, name) dispatcher.connect(self._post_init, signals.post_init, sender=cls) def _post_init(self, instance=None): """Get dynamic upload_to value from the model instance.""" if hasattr(instance, 'get_upload_to'): self.upload_to = instance.get_upload_to(self.attname) def db_type(self): """Required by Django for ORM.""" return 'varchar(100)'
db_type method which replaces the
get_internal_type method in Django trunk. It is used when you run
manage.py syncdb to know what field type to create in the database.
The field is used in a model like this:
class Photo(models.Model): caption = models.CharField(blank=True, maxlength=100) image = CustomImageField(upload_to='photos') def get_upload_to(self, field_attname): """Get upload_to path specific to this photo.""" return 'photos/%d' % self.id
get_upload_to is passed the
attname of the field in that model (in this case “image”). This is so the model can distinguish between multiple custom image fields.
Ok, so the bit I’ve glossed over is that the model may not have an id at the time
get_upload_to is called. If the model is new and hasn’t been saved you’ll need to save it or work something else out before you can return the dynamic
upload_to path. But that was always the case, so I’m not taking the blame.
In my case, the
Photo model was related to some other model (e.g.
Room) which I called to get the path. It didn’t matter that
Photo didn’t have an id because it was related to something that did.
So now I get to save images to paths like:
Update – 20 May 2008:
Here’s a small update to the
CustomImageField class. The version above listens for the
post_init signal and use it to get the dynamic upload path. This works fine when you use it like:
photo = Photo.objects.create(...)
create saves the object and loads it so that
post_init gets called. However, if you create the model object and upload a file before saving, it will not know about the dynamic upload path.
The version below listens for the
pre_save signal instead and gets the dynamic upload path at that point. You can use it like:
photo = Photo() photo.save_image_file(filename, content)
Note that you may still need to save the model before uploading if your dynamic path includes the model id (which is not set until it is saved).
Here is the new version of the field:
class CustomImageField(ImageField): """Allows model instance to specify upload_to dynamically. Model class should have a method like: def get_upload_to(self, attname): return 'path/to/%d' % self.id Based on: http://code.djangoproject.com/wiki/CustomUploadAndFilters """ def __init__(self, *args, **kwargs): if not 'upload_to' in kwargs: kwargs['upload_to'] = 'dummy' self.prime_upload = kwargs.get('prime_upload', False) if 'prime_upload' in kwargs: del(kwargs['prime_upload']) super(CustomImageField, self).__init__(*args, **kwargs) def contribute_to_class(self, cls, name): """Hook up events so we can access the instance.""" super(CustomImageField, self).contribute_to_class(cls, name) if self.prime_upload: dispatcher.connect(self._get_upload_to, signals.post_init, sender=cls) dispatcher.connect(self._get_upload_to, signals.pre_save, sender=cls) def _get_upload_to(self, instance=None): """Get dynamic upload_to value from the model instance.""" if hasattr(instance, 'get_upload_to'): self.upload_to = instance.get_upload_to(self.attname) def db_type(self): """Required by Django for ORM.""" return 'varchar(100)'
In some cases you will still want the path to be specified when the model is initialised rather than saved. If you are editing a model and want to be able to save a new image without saving the model first, it needs to get the dynamic upload path when the
post_init signal is raised.
CustomImageField takes an optional
prime_upload argument. If true, it will also listen for the
post_init event and get the dynamic upload path. You can use it like:
class Photo(models.Model): image = CustomImageField(prime_upload=True)
photo = Photo.objects.get(pk=photo_id) photo.save_image_file(filename, content)
This is all a bit fiddly still, but it does the job until Django has a native way to specify the upload path per-instance.