Custom Upload Fields and Filters

NOTE: Much of this article -- such as altering the names and paths of uploaded files -- is obsolete as of r8244 and the Django 1.0 alpha 2. See the new file documentation for details.

I've made custom file upload fields with some extra features:

  • automatic upload_to path (based on app/model/field names)
  • automatic renaming the filename based on the primary key
  • maximum width and/or height for images

Also, I've created filters to automatically resize/crop images directly from templates.

The original idea came from ImageWithThumbnailField, I've recreated as an exercise and to adapt it to my taste.

There are similar approaches by VERDJN.

Update: added support for verbose name.

Custom upload fields

Here comes the code for the custom upload filters.

The required files are attached.

from django.db.models import ImageField, FileField, signals
from django.dispatch import dispatcher
from django.conf import settings
import shutil, os, glob

# Helpers
from imaging import fit,fit_crop
from fs import change_basename

def auto_rename(file_path, new_name):
    """
    Renames a file, keeping the extension.
    
    Parameters:
        - file_path: the file path relative to MEDIA_ROOT
        - new_name: the new basename of the file (no extension)
    
    Returns the new file path on success or the original file_path on error.
    """
    
    # Return if no file given
    if file_path == '':
        return ''
    #
    
    # Get the new name
    new_path = change_basename(file_path, new_name)
    
    # Changed?
    if new_path != file_path:
        # Try to rename
        try:
            shutil.move(os.path.join(settings.MEDIA_ROOT, file_path), os.path.join(settings.MEDIA_ROOT, new_path))
        except IOError:
            # Error? Restore original name
            new_path = file_path
        #
    #
    
    # Return the new path
    return new_path
# def auto_rename

def auto_resize(file_path, max_width=None, max_height=None, crop=False):
    """
    Resize an image to fit an area.
    Useful to avoid storing large files.
    
    If set to crop, will resize to the closest size and then crop.
    
    At least one of the max_width or max_height parameters must be set.
    """
    
    # Return if no file given or no maximum size passed
    if (not file_path) or ((not max_width) and (not max_height)):
        return
    #
    
    # Get the complete path using MEDIA_ROOT
    real_path = os.path.join(settings.MEDIA_ROOT, file_path)
    
    if (crop):
        fit_crop(real_path, max_width, max_height)
    else:
        fit(real_path, max_width, max_height)
    #
# def auto_resize

def init_path(self, **kwargs):
    """
    Create a flag if there's an 'upload_to' parameter.
    If not found, fill with a dummy value.
    The flag will be used to create an automatic value on "post_init" signal.
    """
    
    # Flag to auto-fill the path if it is empty
    self.fill_path = ('upload_to' not in kwargs)
    
    if self.fill_path:
        # Dummy value to bypass attribute requirement
        kwargs['upload_to'] = '_'
    #
    
    return kwargs
# def init_path

def set_field_path(self, instance = None):
    """
    Set up the "upload_to" for AutoFileField and AutoImageField or "path" for AutoFilePathField.
    Set a path based on the field hierarchy (app/model/field).
    """
    
    # Use the automatic path?
    if self.fill_path:
        setattr(self, 'upload_to', os.path.join(instance._meta.app_label, instance.__class__.__name__, self.name).lower())
    #
# def set_field_path

class AutoFileField(FileField):
    """
    File field with:
    * automatic primary key based renaming
    * automatic upload_to (if not set)
    """
    
    def __init__(self, verbose=None, **kwargs):
        # Adjust the upload_to parameter
        kwargs = init_path(self, **kwargs)
        
        super(AutoFileField, self).__init__(verbose, **kwargs)
    # def __init__
    
    def _post_init(self, instance=None):
        set_field_path(self, instance)
    # def _post_init
    
    def _save(self, instance=None):
        if instance == None:
            return
        filename = auto_rename(getattr(instance, self.attname), '%s' % instance._get_pk_val())
        setattr(instance, self.attname, filename)
    # def _save
    
    def contribute_to_class(self, cls, name):
        super(AutoFileField, self).contribute_to_class(cls, name)
        dispatcher.connect(self._post_init, signals.post_init, sender=cls)
        dispatcher.connect(self._save, signals.pre_save, sender=cls)
    # def contribute_to_class
    
    def get_internal_type(self):
        return 'FileField'
    # def get_internal_type
# class AutoFileField

class AutoImageField(ImageField):
    """
    Image field with:
    * automatic primary key based renaming
    * automatic upload_to (if not set)
    * optional resizing to a maximum width and/or height
    """
    
    def __init__(self, verbose=None, max_width=None, max_height=None, crop=False, **kwargs):
        # Adjust the upload_to parameter
        kwargs = init_path(self, **kwargs)
        
        # Image resizing properties
        self.max_width, self.max_height, self.crop = max_width, max_height, crop
        
        # Set fields for width and height
        self.width_field, self.height_field = 'width', 'height'
        
        super(AutoImageField, self).__init__(verbose, **kwargs)
    # def __init__
    
    def save_file(self, new_data, new_object, original_object, change, rel, save=True):
        # Original method
        super(AutoImageField, self).save_file(new_data, new_object, original_object, change, rel, save)
        
        # Get upload info
        upload_field_name = self.get_manipulator_field_names('')[0]
        field = new_data.get(upload_field_name, False)
        
        # File uploaded?
        if field:
            # Resize image
            auto_resize(getattr(new_object, self.attname), max_width=self.max_width, max_height=self.max_height, crop=self.crop)
        #
    # def save_file
    
    def delete_file(self, instance):
        """
        Deletes left-overs from thumbnail or crop template filters
        """
        
        super(AutoImageField, self).delete_file(instance)
        
        if getattr(instance, self.attname):
            # Get full path
            file_name = getattr(instance, 'get_%s_filename' % self.name)()
            # Get base dir, basename and extension
            basedir = os.path.dirname(file_name)
            base, ext = os.path.splitext(os.path.basename(file_name))
            
            # Delete left-overs from filters
            for file in glob.glob(os.path.join(basedir, base + '_*' + ext)):
                os.remove(os.path.join(basedir, file))
            #
        #
    # def delete_file
    
    def _post_init(self, instance=None):
        set_field_path(self, instance)
    # def _post_init
    
    def _save(self, instance=None):
        if instance == None:
            return
        filename = auto_rename(getattr(instance, self.attname), '%s' % instance._get_pk_val())
        setattr(instance, self.attname, filename)
    # def _save
    
    def contribute_to_class(self, cls, name):
        super(AutoImageField, self).contribute_to_class(cls, name)
        dispatcher.connect(self._post_init, signals.post_init, sender=cls)
        dispatcher.connect(self._save, signals.pre_save, sender=cls)
    # def contribute_to_class

    def get_internal_type(self):
        return 'ImageField'
    # def get_internal_type
# class AutoImageField

Template filters

Here are the automatic image resizing filters.

Remember to adjust your paths to your project.

Update: Because it's not a very good idea to recreate a thumbnail everytime a page with thumbs is loaded, i've added a if not os.path.exists to the code. But dont forget to Overload the save() function of your Model to delete the thumbnail if the Object changes.

from django import template
from django.conf import settings
import os

# Adjust your paths to 'imaging' and 'fs'
from project.custom.imaging import fit,fit_crop
from project.custom.fs import add_to_basename

def parse_args(args = ''):
    """
    Parse filter arguments in the format:
        keyword_1=value_1,keyword_2=value_2
    
    Returns a keyword list
    """
    kwargs = {}
    
    if args:
        for arg in args.split(','):
            kw, val = arg.split('=', 1)
            kwargs[kw.lower()] = val
        # for
    #
    
    return kwargs
# def parse_args

def resize(url, args = '', crop = False):
    """
    On-the-fly thumbnail or crop creation
    """
    
    kwargs = parse_args(args)
    call_kwargs = {}

    if ('width' not in kwargs) and ('height' not in kwargs):
        return url
    #
    
    if crop:
        # Mark as a cropped image
        extra = '_c_'
    else:
        # Mark as a thumbnailed image
        extra = '_t_'
    #
    
    # Setup width and/or height
    if 'width' in kwargs:
        extra += 'w' + kwargs['width']
        call_kwargs['max_width'] = kwargs['width']
    #
    if 'height' in kwargs:
        extra += 'h' + kwargs['height']
        call_kwargs['max_height'] = kwargs['height']
    #
    
    # Remove MEDIA_URL
    url = url.replace(settings.MEDIA_URL, '')
    
    new_url = add_to_basename(url, extra)
    call_kwargs['save_as'] = os.path.join(settings.MEDIA_ROOT, new_url)

    if not os.path.exists(call_kwargs['save_as']):
      if crop:
          # Make the cropping
          ok = fit_crop(os.path.join(settings.MEDIA_ROOT, url), **call_kwargs)
      else:
          # Create the thumbnail
          ok = fit(os.path.join(settings.MEDIA_ROOT, url), **call_kwargs)
    else:
      ok = True
    #
        
    # Something wrong with the image processing?
    if not ok:
        # Silently restore the original url
        new_url = url
    #
    
    # Add MEDIA_URL back to the URL and return
    return settings.MEDIA_URL + new_url
    
# def resize

def thumb(url, args=''):
    """
    On-the-fly thumbnail creation
    
    Usage:
        {{ url|thumb:"width=10,height=20" }}
        {{ url|thumb:"width=10" }}
        {{ url|thumb:"height=20" }}
    """
    
    return resize(url, args)
#

def crop(url, args=''):
    """
    On-the-fly image cropping
    
    Usage:
        {{ url|crop:"width=10,height=20" }}
        {{ url|crop:"width=10" }}
        {{ url|crop:"height=20" }}
    """
    
    return resize(url, args, crop=True)
#

register = template.Library()

register.filter('thumb', thumb)
register.filter('crop', crop)

Though this thing is cool, it doesn't work with Django development version.

Last modified 16 years ago Last modified on May 5, 2009, 2:19:56 PM
Note: See TracWiki for help on using the wiki.
Back to Top