From fe803f898163856af13e6f85c8a3e81cce03ccb7 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 10 Oct 2012 00:18:54 -0400 Subject: [PATCH] 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. --- imagekit/models/fields/__init__.py | 24 ++--- imagekit/models/fields/utils.py | 23 ----- imagekit/models/receivers.py | 51 ----------- imagekit/{specs.py => specs/__init__.py} | 19 ++++ imagekit/specs/signals.py | 5 ++ imagekit/specs/sources.py | 107 +++++++++++++++++++++++ imagekit/utils.py | 8 -- 7 files changed, 139 insertions(+), 98 deletions(-) delete mode 100644 imagekit/models/receivers.py rename imagekit/{specs.py => specs/__init__.py} (91%) create mode 100644 imagekit/specs/signals.py create mode 100644 imagekit/specs/sources.py diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index fb3f78d..92a25d3 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -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 # ":_" @@ -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): """ diff --git a/imagekit/models/fields/utils.py b/imagekit/models/fields/utils.py index 1b3ccaa..3014b53 100644 --- a/imagekit/models/fields/utils.py +++ b/imagekit/models/fields/utils.py @@ -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 diff --git a/imagekit/models/receivers.py b/imagekit/models/receivers.py deleted file mode 100644 index 67960f7..0000000 --- a/imagekit/models/receivers.py +++ /dev/null @@ -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) diff --git a/imagekit/specs.py b/imagekit/specs/__init__.py similarity index 91% rename from imagekit/specs.py rename to imagekit/specs/__init__.py index 2d4b400..160612f 100644 --- a/imagekit/specs.py +++ b/imagekit/specs/__init__.py @@ -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] diff --git a/imagekit/specs/signals.py b/imagekit/specs/signals.py new file mode 100644 index 0000000..7c0888c --- /dev/null +++ b/imagekit/specs/signals.py @@ -0,0 +1,5 @@ +from django.dispatch import Signal + +source_created = Signal(providing_args=[]) +source_changed = Signal() +source_deleted = Signal() diff --git a/imagekit/specs/sources.py b/imagekit/specs/sources.py new file mode 100644 index 0000000..88e6156 --- /dev/null +++ b/imagekit/specs/sources.py @@ -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() diff --git a/imagekit/utils.py b/imagekit/utils.py index fb60035..07f05e5 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -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