mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-04-09 16:00:59 +00:00
Separate source groups and cacheables.
This allows a sensible specialized interface for source groups, but also for ImageKit to interact with specs using the generalized image generator interface.
This commit is contained in:
parent
5b44564318
commit
3931b552a0
8 changed files with 157 additions and 79 deletions
|
|
@ -103,7 +103,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile):
|
|||
super(GeneratedImageCacheFile, self).__init__(storage=storage)
|
||||
|
||||
def _require_file(self):
|
||||
before_access.send(sender=self, generator=self.generator, file=self)
|
||||
before_access.send(sender=self, file=self)
|
||||
return super(GeneratedImageCacheFile, self)._require_file()
|
||||
|
||||
def clear(self):
|
||||
|
|
|
|||
|
|
@ -46,11 +46,6 @@ class StrategyWrapper(object):
|
|||
strategy = strategy()
|
||||
self._wrapped = strategy
|
||||
|
||||
def invoke_callback(self, name, *args, **kwargs):
|
||||
func = getattr(self._wrapped, name, None)
|
||||
if func:
|
||||
func(*args, **kwargs)
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self._wrapped)
|
||||
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@ class ImageSpecField(SpecHostField):
|
|||
self.set_spec_id(cls, name)
|
||||
|
||||
# Add the model and field as a source for this spec id
|
||||
register.cacheables(self.spec_id,
|
||||
ImageFieldSourceGroup(cls, self.source))
|
||||
register.source_group(self.spec_id,
|
||||
ImageFieldSourceGroup(cls, self.source))
|
||||
|
||||
|
||||
class ProcessedImageField(models.ImageField, SpecHostField):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from .exceptions import AlreadyRegistered, NotRegistered
|
||||
from .signals import (before_access, source_created, source_changed,
|
||||
source_deleted)
|
||||
from .signals import before_access, source_created, source_changed, source_deleted
|
||||
from .utils import call_strategy_method
|
||||
|
||||
|
||||
class GeneratorRegistry(object):
|
||||
|
|
@ -12,6 +12,7 @@ class GeneratorRegistry(object):
|
|||
"""
|
||||
def __init__(self):
|
||||
self._generators = {}
|
||||
before_access.connect(self.before_access_receiver)
|
||||
|
||||
def register(self, id, generator):
|
||||
if id in self._generators:
|
||||
|
|
@ -41,6 +42,67 @@ class GeneratorRegistry(object):
|
|||
def get_ids(self):
|
||||
return self._generators.keys()
|
||||
|
||||
def before_access_receiver(self, sender, file, **kwargs):
|
||||
generator = file.generator
|
||||
if generator in self._generators.values():
|
||||
# Only invoke the strategy method for registered generators.
|
||||
call_strategy_method(generator, 'before_access', file=file)
|
||||
|
||||
|
||||
class SourceGroupRegistry(object):
|
||||
"""
|
||||
The source group registry is responsible for listening to source_* signals
|
||||
on source groups, and relaying them to the image cache strategies of the
|
||||
appropriate generators.
|
||||
|
||||
In addition, registering a new source group also registers its cacheables
|
||||
generator with the cacheable registry.
|
||||
|
||||
"""
|
||||
_signals = {
|
||||
source_created: 'on_source_created',
|
||||
source_changed: 'on_source_changed',
|
||||
source_deleted: 'on_source_deleted',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._source_groups = {}
|
||||
for signal in self._signals.keys():
|
||||
signal.connect(self.source_group_receiver)
|
||||
|
||||
def register(self, generator_id, source_group):
|
||||
from .specs.sourcegroups import SourceGroupCacheablesGenerator
|
||||
generator_ids = self._source_groups.setdefault(source_group, set())
|
||||
generator_ids.add(generator_id)
|
||||
cacheable_registry.register(generator_id,
|
||||
SourceGroupCacheablesGenerator(source_group, generator_id))
|
||||
|
||||
def unregister(self, generator_id, source_group):
|
||||
from .specs.sourcegroups import SourceGroupCacheablesGenerator
|
||||
generator_ids = self._source_groups.setdefault(source_group, set())
|
||||
if generator_id in generator_ids:
|
||||
generator_ids.remove(generator_id)
|
||||
cacheable_registry.unregister(generator_id,
|
||||
SourceGroupCacheablesGenerator(source_group, generator_id))
|
||||
|
||||
def source_group_receiver(self, sender, source, signal, **kwargs):
|
||||
"""
|
||||
Relay source group signals to the appropriate spec strategy.
|
||||
|
||||
"""
|
||||
source_group = sender
|
||||
|
||||
# Ignore signals from unregistered groups.
|
||||
if source_group not in self._source_groups:
|
||||
return
|
||||
|
||||
specs = [generator_registry.get(id, source=source) for id in
|
||||
self._source_groups[source_group]]
|
||||
callback_name = self._signals[signal]
|
||||
|
||||
for spec in specs:
|
||||
call_strategy_method(spec, callback_name, file=source)
|
||||
|
||||
|
||||
class CacheableRegistry(object):
|
||||
"""
|
||||
|
|
@ -53,17 +115,8 @@ class CacheableRegistry(object):
|
|||
|
||||
"""
|
||||
|
||||
_signals = [
|
||||
source_created,
|
||||
source_changed,
|
||||
source_deleted,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self._cacheables = {}
|
||||
for signal in self._signals:
|
||||
signal.connect(self.cacheable_receiver)
|
||||
before_access.connect(self.before_access_receiver)
|
||||
|
||||
def register(self, generator_id, cacheables):
|
||||
"""
|
||||
|
|
@ -90,28 +143,6 @@ class CacheableRegistry(object):
|
|||
for cacheable in k():
|
||||
yield cacheable
|
||||
|
||||
def before_access_receiver(self, sender, generator, cacheable, **kwargs):
|
||||
generator.image_cache_strategy.invoke_callback('before_access', cacheable)
|
||||
|
||||
def cacheable_receiver(self, sender, cacheable, signal, info, **kwargs):
|
||||
"""
|
||||
Redirects signals dispatched on cacheables
|
||||
to the appropriate generators.
|
||||
|
||||
"""
|
||||
cacheable = sender
|
||||
if cacheable not in self._cacheables:
|
||||
return
|
||||
|
||||
for generator in (generator_registry.get(id, cacheable=cacheable, **info)
|
||||
for id in self._cacheables[cacheable]):
|
||||
event_name = {
|
||||
source_created: 'source_created',
|
||||
source_changed: 'source_changed',
|
||||
source_deleted: 'source_deleted',
|
||||
}
|
||||
generator._handle_cacheable_event(event_name, cacheable)
|
||||
|
||||
|
||||
class Register(object):
|
||||
"""
|
||||
|
|
@ -132,6 +163,9 @@ class Register(object):
|
|||
def cacheables(self, generator_id, cacheables):
|
||||
cacheable_registry.register(generator_id, cacheables)
|
||||
|
||||
def source_group(self, generator_id, source_group):
|
||||
source_group_registry.register(generator_id, source_group)
|
||||
|
||||
|
||||
class Unregister(object):
|
||||
"""
|
||||
|
|
@ -144,8 +178,12 @@ class Unregister(object):
|
|||
def cacheables(self, generator_id, cacheables):
|
||||
cacheable_registry.unregister(generator_id, cacheables)
|
||||
|
||||
def source_group(self, generator_id, source_group):
|
||||
source_group_registry.unregister(generator_id, source_group)
|
||||
|
||||
|
||||
generator_registry = GeneratorRegistry()
|
||||
cacheable_registry = CacheableRegistry()
|
||||
source_group_registry = SourceGroupRegistry()
|
||||
register = Register()
|
||||
unregister = Unregister()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
from django.dispatch import Signal
|
||||
|
||||
|
||||
# "Cacheables" (cache file) signals
|
||||
before_access = Signal()
|
||||
source_created = Signal(providing_args=[])
|
||||
|
||||
# Source group signals
|
||||
source_created = Signal()
|
||||
source_changed = Signal()
|
||||
source_deleted = Signal()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ from django.db.models.fields.files import ImageFieldFile
|
|||
from hashlib import md5
|
||||
import os
|
||||
import pickle
|
||||
from ..files import GeneratedImageCacheFile
|
||||
from ..imagecache.backends import get_default_image_cache_backend
|
||||
from ..imagecache.strategies import StrategyWrapper
|
||||
from ..processors import ProcessorPipeline
|
||||
|
|
@ -43,11 +42,6 @@ class BaseImageSpec(object):
|
|||
def generate(self):
|
||||
raise NotImplementedError
|
||||
|
||||
# TODO: I don't like this interface. Is there a standard Python one? pubsub?
|
||||
def _handle_source_event(self, event_name, source):
|
||||
file = GeneratedImageCacheFile(self)
|
||||
self.image_cache_strategy.invoke_callback('on_%s' % event_name, file)
|
||||
|
||||
|
||||
class ImageSpec(BaseImageSpec):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,12 +1,26 @@
|
|||
"""
|
||||
Source groups are the means by which image spec sources are identified. They
|
||||
have two responsibilities:
|
||||
|
||||
1. To dispatch ``source_created``, ``source_changed``, and ``source_deleted``
|
||||
signals. (These will be relayed to the corresponding specs' image cache
|
||||
strategies.)
|
||||
2. To provide the source files that they represent, via a generator method named
|
||||
``files()``. (This is used by the warmimagecache management command for
|
||||
"pre-caching" image files.)
|
||||
|
||||
"""
|
||||
|
||||
from django.db.models.signals import post_init, post_save, post_delete
|
||||
from django.utils.functional import wraps
|
||||
from ..files import LazyGeneratedImageCacheFile
|
||||
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 ImageFieldSourceGroup
|
||||
have fields that function as ImageFieldSourceGroup sources.
|
||||
|
||||
"""
|
||||
@wraps(fn)
|
||||
|
|
@ -18,8 +32,15 @@ def ik_model_receiver(fn):
|
|||
|
||||
class ModelSignalRouter(object):
|
||||
"""
|
||||
Handles signals dispatched by models and relays them to the spec source
|
||||
groups that represent those models.
|
||||
Normally, ``ImageFieldSourceGroup`` would be directly responsible for
|
||||
watching for changes on the model field it represents. However, Django does
|
||||
not dispatch events for abstract base classes. Therefore, we must listen for
|
||||
the signals on all models and filter out those that aren't represented by
|
||||
``ImageFieldSourceGroup``s. This class encapsulates that functionality.
|
||||
|
||||
Related:
|
||||
https://github.com/jdriscoll/django-imagekit/issues/126
|
||||
https://code.djangoproject.com/ticket/9318
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -58,25 +79,23 @@ class ModelSignalRouter(object):
|
|||
src in self._source_groups if src.model_class is instance.__class__)
|
||||
|
||||
@ik_model_receiver
|
||||
def post_save_receiver(self, sender, instance=None, created=False,
|
||||
raw=False, **kwargs):
|
||||
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, file, sender,
|
||||
instance, attname)
|
||||
self.dispatch_signal(source_created, file, sender, instance,
|
||||
attname)
|
||||
elif old_hashes[attname] != new_hashes[attname]:
|
||||
self.dispatch_signal(source_changed, file, sender,
|
||||
instance, attname)
|
||||
self.dispatch_signal(source_changed, file, sender, instance,
|
||||
attname)
|
||||
|
||||
@ik_model_receiver
|
||||
def post_delete_receiver(self, sender, instance=None, **kwargs):
|
||||
for attname, file in self.get_field_dict(instance).items():
|
||||
self.dispatch_signal(source_deleted, file, sender, instance,
|
||||
attname)
|
||||
self.dispatch_signal(source_deleted, file, sender, instance, attname)
|
||||
|
||||
@ik_model_receiver
|
||||
def post_init_receiver(self, sender, instance=None, **kwargs):
|
||||
|
|
@ -91,34 +110,55 @@ class ModelSignalRouter(object):
|
|||
"""
|
||||
for source_group in self._source_groups:
|
||||
if source_group.model_class is model_class and source_group.image_field == attname:
|
||||
info = dict(
|
||||
source_group=source_group,
|
||||
instance=instance,
|
||||
field_name=attname,
|
||||
)
|
||||
signal.send(sender=source_group, source=file, info=info)
|
||||
signal.send(sender=source_group, source=file)
|
||||
|
||||
|
||||
class ImageFieldSourceGroup(object):
|
||||
"""
|
||||
A source group that repesents a particular field across all instances of a
|
||||
model.
|
||||
|
||||
"""
|
||||
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)
|
||||
|
||||
def __call__(self):
|
||||
def files(self):
|
||||
"""
|
||||
A generator that returns the source files that this source group
|
||||
represents; in this case, a particular field of every instance of a
|
||||
particular model.
|
||||
|
||||
"""
|
||||
for instance in self.model_class.objects.all():
|
||||
yield {'source': getattr(instance, self.image_field)}
|
||||
yield getattr(instance, self.image_field)
|
||||
|
||||
|
||||
class SourceGroupCacheablesGenerator(object):
|
||||
"""
|
||||
A cacheables generator for source groups. The purpose of this class is to
|
||||
generate cacheables (cache files) from a source group.
|
||||
|
||||
"""
|
||||
def __init__(self, source_group, generator_id):
|
||||
self.source_group = source_group
|
||||
self.generator_id = generator_id
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, self.__class__)
|
||||
and self.__dict__ == other.__dict__)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.source_group, self.generator_id))
|
||||
|
||||
def __call__(self):
|
||||
for source_file in self.source_group.files():
|
||||
yield LazyGeneratedImageCacheFile(self.generator_id,
|
||||
source=source_file)
|
||||
|
||||
|
||||
signal_router = ModelSignalRouter()
|
||||
|
|
|
|||
|
|
@ -422,3 +422,10 @@ def generate(generator):
|
|||
content = f
|
||||
|
||||
return File(content)
|
||||
|
||||
|
||||
def call_strategy_method(generator, method_name, *args, **kwargs):
|
||||
strategy = getattr(generator, 'image_cache_strategy', None)
|
||||
fn = getattr(strategy, method_name, None)
|
||||
if fn is not None:
|
||||
fn(*args, **kwargs)
|
||||
|
|
|
|||
Loading…
Reference in a new issue