Pandemonium Illusion

Django ImageField and FileField dynamic upload path in Newforms Admin

August 6, 2008 · 9 Comments

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)'

Categories: Uncategorized

9 responses so far ↓

  • Hank Sims // August 6, 2008 at 10:35 pm | Reply

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

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

  • jamstooks // August 6, 2008 at 11:17 pm | Reply

    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.

  • Hank Sims // August 7, 2008 at 1:59 am | Reply

    Cool. Thanks again.

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

  • Hank Sims // August 7, 2008 at 2:04 am | Reply

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

    Changeset 8223.

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

  • jamstooks // August 7, 2008 at 2:17 am | Reply

    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!

  • jamstooks // August 7, 2008 at 3:23 am | Reply

    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.

  • Hank Sims // August 7, 2008 at 5:20 pm | Reply

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

  • Hank Sims // August 7, 2008 at 5:21 pm | Reply

    No pre tag, eh?

  • Hank Sims // August 7, 2008 at 6:31 pm | Reply

    Here’s my post-Signal-refactor version.

Leave a Comment