diff --git a/imagekit/conf.py b/imagekit/conf.py index ed568db..288d366 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -8,7 +8,7 @@ class ImageKitConf(AppConf): CACHE_DIR = 'CACHE/images' CACHE_PREFIX = 'imagekit:' DEFAULT_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.JustInTime' - DEFAULT_FILE_STORAGE = None # Indicates that the source storage should be used + DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' def configure_cache_backend(self, value): if value is None: diff --git a/imagekit/files.py b/imagekit/files.py index fddcce2..d92389a 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -95,7 +95,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): self.args = args self.kwargs = kwargs storage = getattr(generator, 'storage', None) - if not storage and settings.IMAGEKIT_DEFAULT_FILE_STORAGE: + if not storage: storage = get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, 'file storage backend') super(GeneratedImageCacheFile, self).__init__(storage=storage) diff --git a/imagekit/registry.py b/imagekit/registry.py index 5952b4e..4147f87 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -132,7 +132,7 @@ class Register(object): """ def spec(self, id, spec): - generator_registry.register(id, spec) + generator_registry.register(spec, id) def sources(self, spec_id, sources): source_group_registry.register(spec_id, sources) diff --git a/imagekit/templatetags/compat.py b/imagekit/templatetags/compat.py new file mode 100644 index 0000000..8334dec --- /dev/null +++ b/imagekit/templatetags/compat.py @@ -0,0 +1,160 @@ +""" +This module contains code from django.template.base +(sha 90d3af380e8efec0301dd91600c6686232de3943). Bundling this code allows us to +support older versions of Django that did not contain it (< 1.4). + + +Copyright (c) Django Software Foundation and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Django nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" + +from django.template import TemplateSyntaxError +import re + + +# Regex for token keyword arguments +kwarg_re = re.compile(r"(?:(\w+)=)?(.+)") + + +def token_kwargs(bits, parser, support_legacy=False): + """ + A utility method for parsing token keyword arguments. + + :param bits: A list containing remainder of the token (split by spaces) + that is to be checked for arguments. Valid arguments will be removed + from this list. + + :param support_legacy: If set to true ``True``, the legacy format + ``1 as foo`` will be accepted. Otherwise, only the standard ``foo=1`` + format is allowed. + + :returns: A dictionary of the arguments retrieved from the ``bits`` token + list. + + There is no requirement for all remaining token ``bits`` to be keyword + arguments, so the dictionary will be returned as soon as an invalid + argument format is reached. + """ + if not bits: + return {} + match = kwarg_re.match(bits[0]) + kwarg_format = match and match.group(1) + if not kwarg_format: + if not support_legacy: + return {} + if len(bits) < 3 or bits[1] != 'as': + return {} + + kwargs = {} + while bits: + if kwarg_format: + match = kwarg_re.match(bits[0]) + if not match or not match.group(1): + return kwargs + key, value = match.groups() + del bits[:1] + else: + if len(bits) < 3 or bits[1] != 'as': + return kwargs + key, value = bits[2], bits[0] + del bits[:3] + kwargs[key] = parser.compile_filter(value) + if bits and not kwarg_format: + if bits[0] != 'and': + return kwargs + del bits[:1] + return kwargs + + +def parse_bits(parser, bits, params, varargs, varkw, defaults, + takes_context, name): + """ + Parses bits for template tag helpers (simple_tag, include_tag and + assignment_tag), in particular by detecting syntax errors and by + extracting positional and keyword arguments. + """ + if takes_context: + if params[0] == 'context': + params = params[1:] + else: + raise TemplateSyntaxError( + "'%s' is decorated with takes_context=True so it must " + "have a first argument of 'context'" % name) + args = [] + kwargs = {} + unhandled_params = list(params) + for bit in bits: + # First we try to extract a potential kwarg from the bit + kwarg = token_kwargs([bit], parser) + if kwarg: + # The kwarg was successfully extracted + param, value = list(kwarg.items())[0] + if param not in params and varkw is None: + # An unexpected keyword argument was supplied + raise TemplateSyntaxError( + "'%s' received unexpected keyword argument '%s'" % + (name, param)) + elif param in kwargs: + # The keyword argument has already been supplied once + raise TemplateSyntaxError( + "'%s' received multiple values for keyword argument '%s'" % + (name, param)) + else: + # All good, record the keyword argument + kwargs[str(param)] = value + if param in unhandled_params: + # If using the keyword syntax for a positional arg, then + # consume it. + unhandled_params.remove(param) + else: + if kwargs: + raise TemplateSyntaxError( + "'%s' received some positional argument(s) after some " + "keyword argument(s)" % name) + else: + # Record the positional argument + args.append(parser.compile_filter(bit)) + try: + # Consume from the list of expected positional arguments + unhandled_params.pop(0) + except IndexError: + if varargs is None: + raise TemplateSyntaxError( + "'%s' received too many positional arguments" % + name) + if defaults is not None: + # Consider the last n params handled, where n is the + # number of defaults. + unhandled_params = unhandled_params[:-len(defaults)] + if unhandled_params: + # Some positional arguments were not supplied + raise TemplateSyntaxError( + "'%s' did not receive value(s) for the argument(s): %s" % + (name, ", ".join(["'%s'" % p for p in unhandled_params]))) + return args, kwargs diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index eea4f8c..f114a80 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -1,134 +1,79 @@ from django import template -from django.utils.safestring import mark_safe -import re -from ..files import ImageSpecCacheFile +from .compat import parse_bits +from ..files import GeneratedImageCacheFile from ..registry import generator_registry register = template.Library() -html_attr_pattern = r""" - (?P\w+) # The attribute name - ( - \s*=\s* # an equals sign, that may or may not have spaces around it - (?P - ("[^"]*") # a double-quoted value - | # or - ('[^']*') # a single-quoted value - | # or - ([^"'<>=\s]+) # an unquoted value - ) - )? -""" +class GenerateImageAssignmentNode(template.Node): + _kwarg_map = { + 'from': 'source_file', + } -html_attr_re = re.compile(html_attr_pattern, re.VERBOSE) - - -class SpecResultNodeMixin(object): - def __init__(self, spec_id, source_file): - self._spec_id = spec_id - self._source_file = source_file - - def get_spec(self, context): - from ..utils import autodiscover - autodiscover() - spec_id = self._spec_id.resolve(context) - spec = generator_registry.get(spec_id) # TODO: What "hints" here? - return spec - - def get_source_file(self, context): - return self._source_file.resolve(context) - - def get_file(self, context): - spec = self.get_spec(context) - source_file = self.get_source_file(context) - return ImageSpecCacheFile(spec, source_file) - - -class SpecResultAssignmentNode(template.Node, SpecResultNodeMixin): - def __init__(self, spec_id, source_file, variable_name): - super(SpecResultAssignmentNode, self).__init__(spec_id, source_file) + def __init__(self, variable_name, generator_id, **kwargs): + self.generator_id = generator_id + self.kwargs = kwargs self._variable_name = variable_name def get_variable_name(self, context): return unicode(self._variable_name) + def get_kwargs(self, context): + return dict((self._kwarg_map.get(k, k), v.resolve(context)) for k, + v in self.kwargs.items()) + def render(self, context): + from ..utils import autodiscover + autodiscover() + variable_name = self.get_variable_name(context) - context[variable_name] = self.get_file(context) + generator_id = self.generator_id.resolve(context) + kwargs = self.get_kwargs(context) + generator = generator_registry.get(generator_id) + context[variable_name] = GeneratedImageCacheFile(generator, **kwargs) return '' -class SpecResultImgTagNode(template.Node, SpecResultNodeMixin): - def __init__(self, spec_id, source_file, html_attrs): - super(SpecResultImgTagNode, self).__init__(spec_id, source_file) - self._html_attrs = html_attrs - - def get_html_attrs(self, context): - attrs = [] - for attr in self._html_attrs: - match = html_attr_re.search(attr) - if match: - attrs.append((match.group('name'), match.group('value'))) - return attrs - - def get_attr_str(self, k, v): - return k if v is None else '%s=%s' % (k, v) - - def render(self, context): - file = self.get_file(context) - attrs = self.get_html_attrs(context) - attr_dict = dict(attrs) - if not 'width' in attr_dict and not 'height' in attr_dict: - attrs = attrs + [('width', '"%s"' % file.width), - ('height', '"%s"' % file.height)] - attrs = [('src', '"%s"' % file.url)] + attrs - attr_str = ' '.join(self.get_attr_str(k, v) for k, v in attrs) - return mark_safe(u'' % attr_str) - - #@register.tag -# TODO: Should this be renamed to something like 'process'? -def spec(parser, token): +def generateimage(parser, token): """ - Creates an image based on the provided spec and source image. - - By default:: - - {% spec 'myapp:thumbnail' mymodel.profile_image %} - - Generates an ````:: - - - - Storing it as a context variable allows more flexibility:: - - {% spec 'myapp:thumbnail' mymodel.profile_image as th %} - + Creates an image based on the provided arguments. """ bits = token.split_contents() - tag_name = bits.pop(0) # noqa + tag_name = bits.pop(0) - if len(bits) == 4 and bits[2] == 'as': - return SpecResultAssignmentNode( - parser.compile_filter(bits[0]), # spec id - parser.compile_filter(bits[1]), # source file - parser.compile_filter(bits[3]), # var name - ) - elif len(bits) > 1: - return SpecResultImgTagNode( - parser.compile_filter(bits[0]), # spec id - parser.compile_filter(bits[1]), # source file - bits[2:], # html attributes - ) + if bits[-2] == 'as': + varname = bits[-1] + bits = bits[:-2] + + # (params, varargs, varkwargs, defaults) = getargspec(g) + # (params, 'args', 'kwargs', defaults) = getargspec(g) + + args, kwargs = parse_bits(parser, bits, + ['generator_id'], 'args', 'kwargs', None, + False, tag_name) + if len(args) != 1: + raise template.TemplateSyntaxError("The 'generateimage' tag " + ' requires exactly one unnamed argument.') + generator_id = args[0] + return GenerateImageAssignmentNode(varname, generator_id, **kwargs) else: - raise template.TemplateSyntaxError('\'spec\' tags must be in the form' - ' "{% spec spec_id image %}" or' - ' "{% spec spec_id image' - ' as varname %}"') + raise Exception('!!') + # elif len(bits) > 1: + # return GenerateImageTagNode( + # parser.compile_filter(bits[0]), # spec id + # parser.compile_filter(bits[1]), # source file + # bits[2:], # html attributes + # ) + # else: + # raise template.TemplateSyntaxError('\'generateimage\' tags must be in the form' + # ' "{% generateimage id image %}" or' + # ' "{% generateimage id image' + # ' as varname %}"') -spec = spec_tag = register.tag(spec) +generateimage = register.tag(generateimage) diff --git a/imagekit/utils.py b/imagekit/utils.py index 7495238..ac93cf6 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -371,7 +371,7 @@ def autodiscover(): import_module('%s.imagespecs' % app) except: # Decide whether to bubble up this error. If the app just - # doesn't have an admin module, we can ignore the error + # doesn't have an imagespecs module, we can ignore the error # attempting to import it, otherwise we want it to bubble up. if module_has_submodule(mod, 'imagespecs'): raise