I’ve been using Scott’s CustomImageField for quite some time, but recently it stopped working when I moved to Newforms Admin. I wasn’t really sure why this was, but decided to build a quick workaround for now so I can continue to use the newforms-admin.
Scott’s Field used the post_init and pre_save signals, which is probably the right way… Since this wasn’t working anymore, I decided to use the post_save signal. This way the image or file would be saved to a temporary directory initially and then moved to the appropriate directory afterwards.
Here is how the field would be defined in the model:
class Image(models.Model):
file = CustomImageField(use_key=True, upload_to='tmp')
use_key and upload_to are optional. use_key defaults to False. If it is True then the id of the instance will be used as a prefix for the new file as there is the potential for overwriting now that we are moving the file. upload_to will simply define the temporary directory to upload the files to initially.
Below is the code that defines the CustomImageField. I have this in a file called “custom_fields.py.”
from django.db.models import ImageField, FileField, signals
from django.dispatch import dispatcher
from django.conf import settings
import shutil, os, glob, re
from distutils.dir_util import mkpath
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 closely on: http://scottbarnham.com/blog/2007/07/31/uploading-images-to-a-dynamic-path-with-django/
"""
def __init__(self, *args, **kwargs):
if not 'upload_to' in kwargs:
kwargs['upload_to'] = 'tmp'
self.use_key = kwargs.get('use_key', False)
if 'use_key' in kwargs:
del(kwargs['use_key'])
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)
dispatcher.connect(self._move_image, signal=signals.post_save, sender=cls)
def _move_image(self, instance=None):
"""
Function to move the temporarily uploaded image to a more suitable directory
using the model's get_upload_to() method.
"""
if hasattr(instance, 'get_upload_to'):
src = getattr(instance, self.attname)
if src:
m = re.match(r"%s/(.*)" % self.upload_to, src)
if m:
if self.use_key:
dst = "%s/%d_%s" % (instance.get_upload_to(self.attname), instance.id, m.groups()[0])
else:
dst = "%s/%s" % (instance.get_upload_to(self.attname), m.groups()[0])
basedir = "%s%s/" % (settings.MEDIA_ROOT, os.path.dirname(dst))
mkpath(basedir)
shutil.move("%s%s" % (settings.MEDIA_ROOT, src),"%s%s" % (settings.MEDIA_ROOT, dst))
setattr(instance, self.attname, dst)
instance.save()
def db_type(self):
"""Required by Django for ORM."""
return 'varchar(100)'
Hey there, thanks for doing the heavy lifting on this.
One bug: Where’s the “mkpath” function in _move_image() coming from?
Hey Hank,
No worries… glad it helped.
mkpath comes from distutils, a standard python library (http://docs.python.org/dist/module-distutils.dirutil.html). I updated the code, but forgot to update the imports. The changes are reflected above.
Thanks for pointing that out.
Cool. Thanks again.
By the way, looks like there’s some current action on that ticket you linked.
Whoa, your patch didn’t last very long, though!
Changeset 8223.
Ay yi yi, the road to 1.0 is killing me.
Yeah, looks like this workaround will be short-lived. Makes me wish I hadn’t spent all night writing it the night before an update to trunk!
http://code.djangoproject.com/wiki/BackwardsIncompatibleChanges#Signalrefactoring
This doesn’t look like too tricky a fix. I’ll check it out tomorrow (if that patch hasn’t been added to the trunk). I hope this doesn’t set #6390 back too far.
For what it’s worth, I’m having better luck using os.path.join() in place of the string substitutions in _move_image().
The problem for me is that my MEDIA_ROOT is defined dynamically through os.path.join(), which doesn’t seem to want to give me the trailing slash. I assume our developers set it up this way to ease the movement between development, testing and production machines.
Anyway, here’s what my (working) _move_image() looks like. Now on to the Signals refactor…
def _move_image(self, instance=None):
“”"
Function to move the temporarily uploaded image to a more suitable directory
using the model’s get_upload_to() method.
“”"
if hasattr(instance, ‘get_upload_to’):
src = getattr(instance, self.attname)
if src:
m = re.match(r”%s/(.*)” % self.upload_to, src)
if m:
if self.use_key:
dst = “%s%d_%s” % (instance.get_upload_to(self.attname), instance.id, m.groups()[0])
else:
dst = “%s%s” % (instance.get_upload_to(self.attname), m.groups()[0])
basedir = os.path.join(settings.MEDIA_ROOT, os.path.dirname(dst))
fromdir = os.path.join(settings.MEDIA_ROOT, src)
shutil.move(fromdir, basedir)
setattr(instance, self.attname, dst)
instance.save()
No pre tag, eh?
Here’s my post-Signal-refactor version.
I had model like this.
…
user = models.ForeignKey(User, related_name=”avatars”)
image = CustomImageField(upload_to=”avatars”)
def get_upload_to(self, field_attname):
return self.user.username
…
but the resulting path is always avatars/aaa.jpg. I wanted the path like avatars/ted/aaa.jpg. what I did wrong here?