Django ImageField and FileField dynamic upload path in Newforms Admin

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)'
About these ads

10 responses to “Django ImageField and FileField dynamic upload path in Newforms Admin

  1. Hey there, thanks for doing the heavy lifting on this.

    One bug: Where’s the “mkpath” function in _move_image() coming from?

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

  3. Cool. Thanks again.

    By the way, looks like there’s some current action on that ticket you linked.

  4. Whoa, your patch didn’t last very long, though!

    Changeset 8223.

    Ay yi yi, the road to 1.0 is killing me.

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

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

  7. 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()

  8. 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?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s