Add image cache strategies

This new feature gives the user more control over *when* their images
are validated. Image cache backends are now exclusively for controlling
the *how*. This means you won't have to write a lot of code when you
just want to change one or the other.
This commit is contained in:
Matthew Tretter 2012-10-03 22:23:11 -04:00
parent f43bd4ec28
commit ba9bf1f877
9 changed files with 100 additions and 51 deletions

View file

@ -1,9 +1,10 @@
from appconf import AppConf
from .imagecache.actions import validate_now, clear_now
class ImageKitConf(AppConf):
DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.backends.Simple'
CACHE_BACKEND = None
VALIDATE_ON_ACCESS = True
CACHE_DIR = 'CACHE/images'
CACHE_PREFIX = 'ik-'
DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.Pessimistic'

View file

@ -56,7 +56,7 @@ class SpecFileGenerator(object):
self.format,
pickle.dumps(self.options),
str(self.autoconvert),
])).hexdigest()
]).encode('utf-8')).hexdigest()
def generate_filename(self, source_file):
source_filename = source_file.name

View file

@ -0,0 +1,23 @@
def validate_now(file):
print 'validate now!'
file.validate()
try:
from celery.task import task
except ImportError:
pass
else:
validate_now_task = task(validate_now)
def deferred_validate(file):
try:
import celery
except:
raise ImportError("Deferred validation requires the the 'celery' library")
validate_now_task.delay(file)
def clear_now(file):
file.clear()

View file

@ -1,4 +1,4 @@
from ...utils import get_singleton
from ..utils import get_singleton
from django.core.cache import get_cache
from django.core.cache.backends.dummy import DummyCache
from django.core.exceptions import ImproperlyConfigured

View file

@ -1,2 +0,0 @@
from .base import *
from .celery import *

View file

@ -1,35 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from .base import InvalidImageCacheBackendError, Simple as SimpleBackend
def generate(model, pk, attr):
try:
instance = model._default_manager.get(pk=pk)
except model.DoesNotExist:
pass # The model was deleted since the task was scheduled. NEVER MIND!
else:
field_file = getattr(instance, attr)
field_file.delete(save=False)
field_file.generate(save=True)
class CeleryBackend(SimpleBackend):
"""
An image cache backend that uses celery to generate images.
"""
def __init__(self):
try:
from celery.task import task
except:
raise InvalidImageCacheBackendError("Celery validation backend requires the 'celery' library")
if not getattr(CeleryBackend, '_task', None):
CeleryBackend._task = task(generate)
def invalidate(self, file):
self._task.delay(file.instance.__class__, file.instance.pk, file.attname)
def clear(self, file):
file.delete(save=False)

View file

@ -0,0 +1,57 @@
from .actions import validate_now, clear_now
from ..utils import get_singleton
class Pessimistic(object):
"""
A caching strategy that validates the file every time it's accessed.
"""
def on_access(self, file):
validate_now(file)
def on_source_delete(self, file):
clear_now(file)
def on_source_change(self, file):
validate_now(file)
class Optimistic(object):
"""
A caching strategy that validates when the source file changes and assumes
that the cached file will persist.
"""
def on_source_create(self, file):
validate_now(file)
def on_source_delete(self, file):
clear_now(file)
def on_source_change(self, file):
validate_now(file)
class DictStrategy(object):
def __init__(self, callbacks):
for k, v in callbacks.items():
setattr(self, k, v)
class StrategyWrapper(object):
def __init__(self, strategy):
if isinstance(strategy, basestring):
strategy = get_singleton(strategy, 'image cache strategy')
elif isinstance(strategy, dict):
strategy = DictStrategy(strategy)
elif callable(strategy):
strategy = strategy()
self._wrapped = strategy
def invoke_callback(self, name, *args, **kwargs):
func = getattr(self._wrapped, 'on_%s' % name, None)
if func:
func(*args, **kwargs)

View file

@ -5,6 +5,7 @@ from django.db import models
from django.db.models.signals import post_init, post_save, post_delete
from ...imagecache.backends import get_default_image_cache_backend
from ...imagecache.strategies import StrategyWrapper
from ...generators import SpecFileGenerator
from .files import ImageSpecFieldFile, ProcessedImageFieldFile
from .utils import ImageSpecFileDescriptor, ImageKitMeta, BoundImageKitMeta
@ -19,7 +20,7 @@ class ImageSpecField(object):
"""
def __init__(self, processors=None, format=None, options=None,
image_field=None, pre_cache=None, storage=None, autoconvert=True,
image_cache_backend=None, validate_on_access=None):
image_cache_backend=None, image_cache_strategy=None):
"""
:param processors: A list of processors to run on the original image.
:param format: The format of the output file. If not provided,
@ -38,9 +39,10 @@ class ImageSpecField(object):
``prepare_image()`` should be performed prior to saving.
:param image_cache_backend: An object responsible for managing the state
of cached files. Defaults to an instance of
IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND
:param validate_on_access: Should the image cache be validated when it's
accessed?
``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND``
:param image_cache_strategy: A dictionary containing callbacks that
allow you to customize how and when the image cache is validated.
Defaults to ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY``
"""
@ -61,8 +63,9 @@ class ImageSpecField(object):
self.storage = storage
self.image_cache_backend = image_cache_backend or \
get_default_image_cache_backend()
self.validate_on_access = settings.IMAGEKIT_VALIDATE_ON_ACCESS if \
validate_on_access is None else validate_on_access
if image_cache_strategy is None:
image_cache_strategy = settings.IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY
self.image_cache_strategy = StrategyWrapper(image_cache_strategy)
def contribute_to_class(self, cls, name):
setattr(cls, name, ImageSpecFileDescriptor(self, name))
@ -101,8 +104,11 @@ class ImageSpecField(object):
old_hashes = instance._ik._source_hashes.copy()
new_hashes = ImageSpecField._update_source_hashes(instance)
for attname in instance._ik.spec_fields:
if old_hashes[attname] != new_hashes[attname]:
getattr(instance, attname).invalidate()
file = getattr(instance, attname)
if created:
file.field.image_cache_strategy.invoke_callback('source_create', file)
elif old_hashes[attname] != new_hashes[attname]:
file.field.image_cache_strategy.invoke_callback('source_change', file)
@staticmethod
def _update_source_hashes(instance):
@ -119,7 +125,7 @@ class ImageSpecField(object):
@staticmethod
def _post_delete_receiver(sender, instance=None, **kwargs):
for spec_file in instance._ik.spec_files:
spec_file.clear()
spec_file.field.image_cache_strategy.invoke_callback('source_delete', spec_file)
@staticmethod
def _post_init_receiver(sender, instance, **kwargs):

View file

@ -34,8 +34,7 @@ class ImageSpecFieldFile(ImageFieldFile):
def _require_file(self):
if not self.source_file:
raise ValueError("The '%s' attribute's image_field has no file associated with it." % self.attname)
elif self.field.validate_on_access:
self.validate()
self.field.image_cache_strategy.invoke_callback('access', self)
def clear(self):
return self.field.image_cache_backend.clear(self)