Permalink
Browse files

improved path traversal and type checking for undermythumb fields.

- ImageFallbackField performs a better type check to determine
  if its content is a real field upload or the result of a fallback.
- BaseRenderer now uses a default quality of "100". "75" is ridiculously low.
- abstracted fallback logic into separate function
- scrapped current tests in favor of a simple approach. more tests coming soon.
- cleaned up test runner, models, and settings
  • Loading branch information...
1 parent aab5d20 commit 35b23c48360b60455b05b436f6e885a2c221d8af @mattdennewitz mattdennewitz committed Aug 31, 2011
View
@@ -38,7 +38,6 @@ def _populate(self):
except ValueError:
attname, renderer = options
key = attname
-
ext = '.%s' % renderer.format
name = self.field.get_thumbnail_filename(
@@ -71,78 +70,80 @@ def __iter__(self):
yield value
+def traverse_fallback_path(instance, fallback_path):
+ """Ramble down a dotted path, looking for the end of the road.
+
+ Break down the path, and traverse.
+ If the path is ``article_header.thumbnails.list``,
+ the order would be: ``article_header -> thumbnails -> list``.
+
+ See also: http://en.wikipedia.org/wiki/The_Hunt_(The_Twilight_Zone)
+ """
+
+ value = instance
+ path_bits = fallback_path.split('.')
+
+ while path_bits:
+ bit = path_bits.pop(0)
+
+ try:
+ bit = int(bit)
+ value = value[bit]
+ except IndexError:
+ value = None
+ break
+ except ValueError:
+ if isinstance(value, dict):
+ value = value[bit]
+ else:
+ value = getattr(value, bit, None)
+ if callable(value):
+ value = value()
+
+ return value
+
+
class FallbackFieldDescriptor(ImageFileDescriptor):
def __get__(self, instance, owner):
- """Returns either the custom thumbnail provided directly
- to this field, or the thumbnail from the mirror field.
+ """Returns a field's image. If no image is found, this descriptor
+ inspects and traverses its field's ``fallback_path``, to find and return
+ whatever lies at the end of the path.
"""
# if this particular field is empty, return the url
# of the mirror field's thumbnail
- value = super(FallbackFieldDescriptor, self).__get__(instance,
- owner)
+ value = super(FallbackFieldDescriptor, self).__get__(instance, owner)
# if given a real value, mark as non-empty and return
- # for saving to the database
- if (type(value) in (ImageFieldFile,
- ThumbnailFieldFile,
- ImageWithThumbnailsFieldFile,
- ImageFallbackField)
- and hasattr(value, 'url')):
+ if (isinstance(value, (ImageFieldFile,
+ ThumbnailFieldFile,
+ ImageWithThumbnailsFieldFile,
+ ImageFallbackField))
+ and hasattr(value, 'url')):
value._empty = False
return value
- # this image field has no content
+ # this value has no content. mark it as empty.
value._empty = True
# check to see if this image has a fallback path
# no fallback path? check to see if the field
# has a name, mark as empty/filled, and return.
- #
- # we check the "name" attr here, because the file
- # might not be saved.
if self.field.fallback_path is None:
if getattr(value, 'name'):
value._empty = False
- else:
- value._empty = True
return value
- if callable(self.field.fallback_path):
- # call fallback path function with instance
- fallback_path = self.field.fallback_path(instance)
- else:
- fallback_path = self.field.fallback_path
-
- # break down the path, and traverse.
- # if the path is 'article_header.thumbnails.list',
- # the order would be: article_header -> thumbnails -> list.
- #
- # - numbers are interpreted as list indexes.
- # - ok to use callables in the chain
- # - can't remember if it's been tested on dictionaries,
- # but i don't think it has
- path_bits = fallback_path.split('.')
- mirror_value = instance
-
- while path_bits:
- bit = path_bits.pop(0)
- try:
- bit = int(bit)
- mirror_value = mirror_value[bit]
- except IndexError:
- mirror_value = None
- break
- except ValueError:
- mirror_value = getattr(mirror_value, bit, None)
- if callable(mirror_value):
- mirror_value = mirror_value()
+ # using the instance, trace through the fallback path
+ mirror_value = traverse_fallback_path(instance,
+ self.field.fallback_path)
if mirror_value is None:
return None
mirror_value._empty = True
+
return mirror_value
@@ -194,11 +195,8 @@ def south_field_triple(self):
class ImageFallbackField(ImageField):
- """A special ImageField that allows a user to optionally override
- a particular ImageWithThumbnailsField thumbnail, or transparently
- fall back to another thumbnail if no image is supplied.
-
- No special transformations are applied to uploaded images.
+ """A special ``ImageField`` subclass for defining an image field
+ capable of falling back to the value of another field if empty.
"""
descriptor_class = FallbackFieldDescriptor
@@ -208,21 +206,26 @@ def __init__(self, fallback_path, *args, **kwargs):
self.fallback_path = fallback_path
def get_db_prep_value(self, value, connection, prepared=False):
+ """Ensures that a given value comes from *this* field instance,
+ is not empty, and is *only* an ``ImageFieldFile``.
- if not value:
- return None
-
- if hasattr(value, 'field'):
- if (value.field != self or (hasattr(value, '_empty') and value._empty)):
- return None
+ This logic prevents values from fallback path traversal from
+ being persisted.
+ """
- if not isinstance(value, (ImageFieldFile, ThumbnailFieldFile, basestring)):
+ if not value:
return None
- return unicode(value)
+ # we only want ImageFieldFile instances given to *this* field
+ if ((type(value) == ImageFieldFile) and
+ (value.field == self) and
+ (hasattr(value, '_empty') and not value._empty)):
+ return unicode(value)
+
+ return None
def south_field_triple(self):
- """Return a description of this field for South.
+ """South field descriptor.
"""
from south.modelsinspector import introspector
@@ -11,12 +11,11 @@
class BaseRenderer(object):
+ """Renderer base. Subclass this to build your own renderers.
"""
- You can subclass this to get basic rendering behavior.
- """
- def __init__(self, format='jpg', quality=75, force_rgb=True,
- *args, **kwargs):
+ def __init__(self, format='jpg', quality=100, force_rgb=True,
+ *args, **kwargs):
self.format = format
self.quality = quality
self.force_rgb = force_rgb
Deleted file not rendered
@@ -4,74 +4,28 @@
from django.core.files.storage import FileSystemStorage
from django.db import models
-from undermythumb.fields import ImageWithThumbnailsField, \
- ImageFallbackField
+from undermythumb.fields import ImageWithThumbnailsField, ImageFallbackField
from undermythumb.renderers import CropRenderer
-class Car(models.Model):
- image = ImageWithThumbnailsField(
- upload_to='original',
- storage=FileSystemStorage(settings.TEST_MEDIA_ROOT),
- thumbnails=(
- ('small', CropRenderer(25, 25)),
- ('medium', CropRenderer(50, 50)),
- ('large', CropRenderer(75, 75)),
- ),
- )
+class BlogPost(models.Model):
+ title = models.CharField(max_length=100)
+ # an image with thumbnails
+ artwork = ImageWithThumbnailsField(max_length=255,
+ upload_to='artwork/',
+ thumbnails=(('homepage_image', CropRenderer(300, 150)),
+ ('pagination_image', CropRenderer(150, 75))))
-def original_upload_to(instance, filename):
- base, ext = os.path.splitext(filename)
- return os.path.join(instance.name, 'original%s' % ext)
-
-
-def thumbnails_upload_to(instance, original, key, ext):
- return os.path.join(instance.name, '%s%s' % (key, ext))
-
-
-class Author(models.Model):
- image = ImageWithThumbnailsField(
- upload_to='authors/',
- blank=True, null=True,
- storage=FileSystemStorage(settings.TEST_MEDIA_ROOT),
- thumbnails_storage=FileSystemStorage(settings.TEST_MEDIA_CUSTOM_ROOT),
- thumbnails=(
- ('small', CropRenderer(25, 25, format='png')),
- ('medium', CropRenderer(50, 50)),
- ('large', CropRenderer(75, 75)),
- ),
- )
- small_image = ImageFallbackField(fallback_path='image.thumbnails.small',
- upload_to='authors/')
-
-
-class Book(models.Model):
- author = models.ForeignKey(Author, null=True)
- name = models.CharField(max_length=32)
- author_image = ImageFallbackField('author.image',
- upload_to='authors/',
- null=True)
- alt_author_image = ImageFallbackField('author_image',
- upload_to='authors/',
- null=True)
- image = ImageWithThumbnailsField(
- blank=True, null=True,
- upload_to=original_upload_to,
- storage=FileSystemStorage(settings.TEST_MEDIA_ROOT),
- thumbnails_upload_to=thumbnails_upload_to,
- thumbnails=(
- ('small', CropRenderer(25, 25)),
- ('medium', CropRenderer(50, 50)),
- ('large', CropRenderer(75, 75)),
- ),
- )
- alt_image = ImageWithThumbnailsField(blank=True,
- null=True,
- fallback_path='image',
- upload_to='authors/',
- storage=FileSystemStorage(settings.TEST_MEDIA_ROOT),
- thumbnails=(('small', CropRenderer(25, 25)),
- ('medium', CropRenderer(50, 50)),
- ('large', CropRenderer(75, 75))))
+ # an override field, capable of rolling up a path
+ # when it has no value. useful for overriding
+ # auto-generated thumbnails. the fallback path
+ # should point to an ImageFieldFile of some sort.
+ #
+ # under the hood, this is just an ImageField
+ homepage_image = ImageFallbackField(fallback_path='artwork.thumbnails.homepage_image',
+ upload_to='artwork/')
+ def __unicode__(self):
+ return self.title
+
@@ -9,11 +9,10 @@
parent = os.path.realpath(os.path.join(os.path.dirname(__file__),
os.path.pardir,
os.path.pardir))
-
sys.path.insert(0, parent)
-from django.test.simple import run_tests
from django.conf import settings
+from django.test.simple import run_tests
def runtests():
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,12 @@
+import os
+
+from django.conf import settings
+from django.core.files.storage import FileSystemStorage
+
+
+class FileSystemOverwriteStorage(FileSystemStorage):
+
+ def get_available_name(self, name):
+ if self.exists(name):
+ os.remove(os.path.join(settings.MEDIA_ROOT, name))
+ return name
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -7,3 +7,5 @@
TEST_MEDIA_ROOT = '/tmp/thumbnails-test/'
TEST_MEDIA_CUSTOM_ROOT = '/tmp/thumbnails-test-custom/'
+
+DEFAULT_FILE_STORAGE = 'storage.FileSystemOverwriteStorage'
Oops, something went wrong.

0 comments on commit 35b23c4

Please sign in to comment.