Beginning to move functionality into "sources"

Before this is applied, we're going to have to make it so that the
image cache strategies are passed the source file, not the other file.
This commit is contained in:
Matthew Tretter 2012-10-10 00:18:54 -04:00
parent 06e5f45904
commit fe803f8981
7 changed files with 139 additions and 98 deletions

View file

@ -1,11 +1,12 @@
import os
from django.db import models
from .files import ProcessedImageFieldFile
from .utils import ImageSpecFileDescriptor, ImageKitMeta
from ..files import ProcessedImageFieldFile
from .utils import ImageSpecFileDescriptor
from ..receivers import configure_receivers
from ...utils import suggest_extension
from ...specs import SpecHost
from ...specs import SpecHost, spec_registry
from ...specs.sources import ImageFieldSpecSource
class ImageSpecField(SpecHost):
@ -40,19 +41,6 @@ class ImageSpecField(SpecHost):
def contribute_to_class(self, cls, name):
setattr(cls, name, ImageSpecFileDescriptor(self, name))
try:
# Make sure we don't modify an inherited ImageKitMeta instance
ik = cls.__dict__['ik']
except KeyError:
try:
base = getattr(cls, '_ik')
except AttributeError:
ik = ImageKitMeta()
else:
# Inherit all the spec fields.
ik = ImageKitMeta(base.spec_fields)
setattr(cls, '_ik', ik)
ik.spec_fields.append(name)
# Generate a spec_id to register the spec with. The default spec id is
# "<app>:<model>_<field>"
@ -64,6 +52,10 @@ class ImageSpecField(SpecHost):
# later, from outside of the model definition.
self.set_spec_id(self.spec_id)
# Register the model and field as a source for this spec id
spec_registry.add_source(self.spec_id,
ImageFieldSpecSource(cls, self.image_field))
class ProcessedImageField(models.ImageField, SpecHost):
"""

View file

@ -1,29 +1,6 @@
from .files import ImageSpecFieldFile
class BoundImageKitMeta(object):
def __init__(self, instance, spec_fields):
self.instance = instance
self.spec_fields = spec_fields
@property
def spec_files(self):
return [getattr(self.instance, n) for n in self.spec_fields]
class ImageKitMeta(object):
def __init__(self, spec_fields=None):
self.spec_fields = list(spec_fields) if spec_fields else []
def __get__(self, instance, owner):
if instance is None:
return self
else:
ik = BoundImageKitMeta(instance, self.spec_fields)
setattr(instance, '_ik', ik)
return ik
class ImageSpecFileDescriptor(object):
def __init__(self, field, attname):
self.attname = attname

View file

@ -1,51 +0,0 @@
from django.db.models.signals import post_init, post_save, post_delete
from ..utils import ik_model_receiver
def update_source_hashes(instance):
"""
Stores hashes of the source image files so that they can be compared
later to see whether the source image has changed (and therefore whether
the spec file needs to be regenerated).
"""
instance._ik._source_hashes = dict((f.attname, hash(f.source_file)) \
for f in instance._ik.spec_files)
return instance._ik._source_hashes
@ik_model_receiver
def post_save_receiver(sender, instance=None, created=False, raw=False, **kwargs):
if not raw:
old_hashes = instance._ik._source_hashes.copy()
new_hashes = update_source_hashes(instance)
for attname in instance._ik.spec_fields:
file = getattr(instance, attname)
if created:
file.field.spec.image_cache_strategy.invoke_callback('source_create', file)
elif old_hashes[attname] != new_hashes[attname]:
file.field.spec.image_cache_strategy.invoke_callback('source_change', file)
@ik_model_receiver
def post_delete_receiver(sender, instance=None, **kwargs):
for spec_file in instance._ik.spec_files:
spec_file.field.spec.image_cache_strategy.invoke_callback('source_delete', spec_file)
@ik_model_receiver
def post_init_receiver(sender, instance, **kwargs):
update_source_hashes(instance)
def configure_receivers():
# Connect the signals. We have to listen to every model (not just those
# with IK fields) and filter in our receivers because of a Django issue with
# abstract base models.
# Related:
# https://github.com/jdriscoll/django-imagekit/issues/126
# https://code.djangoproject.com/ticket/9318
uid = 'ik_spec_field_receivers'
post_init.connect(post_init_receiver, dispatch_uid=uid)
post_save.connect(post_save_receiver, dispatch_uid=uid)
post_delete.connect(post_delete_receiver, dispatch_uid=uid)

View file

@ -1,3 +1,4 @@
from collections import defaultdict
from django.conf import settings
from hashlib import md5
import os
@ -7,6 +8,7 @@ from .imagecache.backends import get_default_image_cache_backend
from .imagecache.strategies import StrategyWrapper
from .lib import StringIO
from .processors import ProcessorPipeline
from .signals import source_created, source_changed, source_deleted
from .utils import (open_image, extension_to_format, IKContentFile, img_to_fobj,
suggest_extension)
@ -14,12 +16,29 @@ from .utils import (open_image, extension_to_format, IKContentFile, img_to_fobj,
class SpecRegistry(object):
def __init__(self):
self._specs = {}
self._sources = defaultdict(list)
def register(self, id, spec):
if id in self._specs:
raise AlreadyRegistered('The spec with id %s is already registered' % id)
self._specs[id] = spec
def add_source(self, id, source):
self._sources[id].append(source)
source_created.connect(receiver, sender, weak, dispatch_uid)
source_changed.connect(receiver, sender, weak, dispatch_uid)
source_deleted.connect(receiver, sender, weak, dispatch_uid)
def source_receiver(self, source, source_file):
# Get a list of specs that use this source.
ids = (k for k, v in self._sources.items() if source in v)
specs = (self.get_spec(id) for id in ids)
for spec in specs:
spec.image_cache_strategy.invoke_callback(..., source_file)
def get_sources(self, id):
return self._sources[id]
def unregister(self, id, spec):
try:
del self._specs[id]

View file

@ -0,0 +1,5 @@
from django.dispatch import Signal
source_created = Signal(providing_args=[])
source_changed = Signal()
source_deleted = Signal()

107
imagekit/specs/sources.py Normal file
View file

@ -0,0 +1,107 @@
from django.db.models.signals import post_init, post_save, post_delete
from django.utils.functional import wraps
from .signals import source_created, source_changed, source_deleted
def ik_model_receiver(fn):
"""
A method decorator that filters out signals coming from models that don't
have fields that function as ImageFieldSpecSources
"""
@wraps(fn)
def receiver(self, sender, **kwargs):
if sender in (src.model_class for src in self._sources):
fn(sender, **kwargs)
return receiver
class ModelSignalRouter(object):
def __init__(self):
self._sources = []
uid = 'ik_spec_field_receivers'
post_init.connect(self.post_init_receiver, dispatch_uid=uid)
post_save.connect(self.post_save_receiver, dispatch_uid=uid)
post_delete.connect(self.post_delete_receiver, dispatch_uid=uid)
def add(self, source):
self._sources.append(source)
def init_instance(self, instance):
instance._ik = getattr(instance, '_ik', {})
def update_source_hashes(self, instance):
"""
Stores hashes of the source image files so that they can be compared
later to see whether the source image has changed (and therefore whether
the spec file needs to be regenerated).
"""
self.init_instance(instance)
instance._ik['source_hashes'] = dict((k, hash(v.source_file))
for k, v in self.get_field_dict(instance).items())
return instance._ik['source_hashes']
def get_field_dict(self, instance):
"""
Returns the source fields for the given instance, in a dictionary whose
keys are the field names and values are the fields themselves.
"""
return dict((src.image_field, getattr(instance, src.image_field)) for
src in self._sources if src.model_class is instance.__class__)
@ik_model_receiver
def post_save_receiver(self, sender, instance=None, created=False, raw=False, **kwargs):
if not raw:
self.init_instance(instance)
old_hashes = instance._ik.get('source_hashes', {}).copy()
new_hashes = self.update_source_hashes(instance)
for attname, file in self.get_field_dict(instance).items():
if created:
self.dispatch_signal(source_created, sender, file)
elif old_hashes[attname] != new_hashes[attname]:
self.dispatch_signal(source_changed, sender, file)
@ik_model_receiver
def post_delete_receiver(self, sender, instance=None, **kwargs):
for attname, file in self.get_field_dict(instance):
self.dispatch_signal(source_deleted, sender, file)
@classmethod
@ik_model_receiver
def post_init_receiver(self, sender, instance, **kwargs):
self.update_source_hashes(instance)
def dispatch_signal(self, signal, model_class, file):
"""
Dispatch the signal for each of the matching sources. Note that more
than one source can have the same model and image_field; it's important
that we dispatch the signal for each.
"""
for source in self._sources:
if source.model_class is model_class and source.image_field == file.attname:
signal.send(sender=source, source_file=file)
class ImageFieldSpecSource(object):
def __init__(self, model_class, image_field):
"""
Good design would dictate that this instance would be responsible for
watching for changes for the provided field. However, due to a bug in
Django, we can't do that without leaving abstract base models (which
don't trigger signals) in the lurch. So instead, we do all signal
handling through the signal router.
Related:
https://github.com/jdriscoll/django-imagekit/issues/126
https://code.djangoproject.com/ticket/9318
"""
self.model_class = model_class
self.image_field = image_field
signal_router.add(self)
signal_router = ModelSignalRouter()

View file

@ -399,14 +399,6 @@ def get_singleton(class_path, desc):
return instance
def ik_model_receiver(fn):
@wraps(fn)
def receiver(sender, **kwargs):
if getattr(sender, '_ik', None):
fn(sender, **kwargs)
return receiver
def autodiscover():
"""
Auto-discover INSTALLED_APPS imagespecs.py modules and fail silently when