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.
Attachments (2)
-
fs.py
(939 bytes
) - added by 18 years ago.
Filesystem utilities for custom upload fields
-
imaging.py
(1.7 KB
) - added by 18 years ago.
Image maniupulation utilities for custom upload fields
Download all attachments as: .zip