kopia lustrzana https://github.com/wagtail/wagtail
implemented filter chaining
rodzic
7fee62f8eb
commit
c6b2cef575
|
@ -25,6 +25,7 @@ Changelog
|
|||
* Added a system check to warn developers who use a custom Wagtail build but forgot to build the admin css
|
||||
* Added success message after updating image from the image upload view (Christian Peters)
|
||||
* Added a `request.is_preview` variable for templates to distinguish between previewing and live (Denis Voskvitsov)
|
||||
* Added support for chaining multiple image operations on the `{% image %}` tag (Christian Peters)
|
||||
* New translations for Arabic and Latvian
|
||||
* 'Pages' link on site stats dashboard now links to the site homepage when only one site exists, rather than the root level
|
||||
* Fix: Images and page revisions created by a user are no longer deleted when the user is deleted (Rich Atkinson)
|
||||
|
|
|
@ -71,6 +71,7 @@ Minor features
|
|||
* Added success message after updating image from the image upload view (Christian Peters)
|
||||
* Added a ``request.is_preview`` variable for templates to distinguish between previewing and live (Denis Voskvitsov)
|
||||
* 'Pages' link on site stats dashboard now links to the site homepage when only one site exists, rather than the root level
|
||||
* Added support for chaining multiple image operations on the ``{% image %}`` tag (Christian Peters)
|
||||
* New translations for Arabic and Latvian
|
||||
|
||||
|
||||
|
|
|
@ -262,7 +262,7 @@ class AbstractImage(models.Model, TagSearchable):
|
|||
'gif': '.gif',
|
||||
}
|
||||
|
||||
output_extension = filter.spec + FORMAT_EXTENSIONS[output_format]
|
||||
output_extension = filter.spec.replace('|', '.') + FORMAT_EXTENSIONS[output_format]
|
||||
if cache_key:
|
||||
output_extension = cache_key + '.' + output_extension
|
||||
|
||||
|
@ -360,10 +360,12 @@ def get_image_model():
|
|||
|
||||
class Filter(models.Model):
|
||||
"""
|
||||
Represents an operation that can be applied to an Image to produce a rendition
|
||||
Represents one or more operations that can be applied to an Image to produce a rendition
|
||||
appropriate for final display on the website. Usually this would be a resize operation,
|
||||
but could potentially involve colour processing, etc.
|
||||
"""
|
||||
|
||||
# The spec pattern is operation1-var1-var2|operation2-var1
|
||||
spec = models.CharField(max_length=255, db_index=True, unique=True)
|
||||
|
||||
@cached_property
|
||||
|
@ -373,7 +375,7 @@ class Filter(models.Model):
|
|||
|
||||
# Build list of operation objects
|
||||
operations = []
|
||||
for op_spec in self.spec.split():
|
||||
for op_spec in self.spec.split('|'):
|
||||
op_spec_parts = op_spec.split('-')
|
||||
|
||||
if op_spec_parts[0] not in self._registered_operations:
|
||||
|
@ -381,7 +383,6 @@ class Filter(models.Model):
|
|||
|
||||
op_class = self._registered_operations[op_spec_parts[0]]
|
||||
operations.append(op_class(*op_spec_parts))
|
||||
|
||||
return operations
|
||||
|
||||
def run(self, image, output):
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import re
|
||||
|
||||
from django import template
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
@ -5,33 +7,60 @@ from wagtail.wagtailimages.models import Filter
|
|||
from wagtail.wagtailimages.shortcuts import get_rendition_or_not_found
|
||||
|
||||
register = template.Library()
|
||||
allowed_filter_pattern = re.compile("^[A-Za-z0-9_\-\.]+$")
|
||||
|
||||
|
||||
@register.tag(name="image")
|
||||
def image(parser, token):
|
||||
bits = token.split_contents()[1:]
|
||||
image_expr = parser.compile_filter(bits[0])
|
||||
filter_spec = bits[1]
|
||||
bits = bits[2:]
|
||||
bits = bits[1:]
|
||||
|
||||
if len(bits) == 2 and bits[0] == 'as':
|
||||
# token is of the form {% image self.photo max-320x200 as img %}
|
||||
return ImageNode(image_expr, filter_spec, output_var_name=bits[1])
|
||||
else:
|
||||
# token is of the form {% image self.photo max-320x200 %} - all additional tokens
|
||||
# should be kwargs, which become attributes
|
||||
attrs = {}
|
||||
for bit in bits:
|
||||
filter_specs = []
|
||||
attrs = {}
|
||||
output_var_name = None
|
||||
|
||||
as_context = False # if True, the next bit to be read is the output variable name
|
||||
is_valid = True
|
||||
|
||||
for bit in bits:
|
||||
if bit == 'as':
|
||||
# token is of the form {% image self.photo max-320x200 as img %}
|
||||
as_context = True
|
||||
elif as_context:
|
||||
if output_var_name is None:
|
||||
output_var_name = bit
|
||||
else:
|
||||
# more than one item exists after 'as' - reject as invalid
|
||||
is_valid = False
|
||||
else:
|
||||
try:
|
||||
name, value = bit.split('=')
|
||||
attrs[name] = parser.compile_filter(value) # setup to resolve context variables as value
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError(
|
||||
"""'image' tag should be of the form {% image self.photo max-320x200
|
||||
[ custom-attr=\"value\" ... ] %} or {% image self.photo max-320x200 as img %}"""
|
||||
)
|
||||
attrs[name] = parser.compile_filter(value) # setup to resolve context variables as value
|
||||
if allowed_filter_pattern.match(bit):
|
||||
filter_specs.append(bit)
|
||||
else:
|
||||
raise template.TemplateSyntaxError(
|
||||
"filter specs in 'image' tag may only contain A-Z, a-z, 0-9, dots, hyphens and underscores. "
|
||||
"(given filter: {})".format(bit)
|
||||
)
|
||||
|
||||
return ImageNode(image_expr, filter_spec, attrs=attrs)
|
||||
if as_context and output_var_name is None:
|
||||
# context was introduced but no variable given ...
|
||||
is_valid = False
|
||||
|
||||
if output_var_name and attrs:
|
||||
# attributes are not valid when using the 'as img' form of the tag
|
||||
is_valid = False
|
||||
|
||||
if is_valid:
|
||||
return ImageNode(image_expr, '|'.join(filter_specs), attrs=attrs, output_var_name=output_var_name)
|
||||
else:
|
||||
raise template.TemplateSyntaxError(
|
||||
"'image' tag should be of the form {% image self.photo max-320x200 [ custom-attr=\"value\" ... ] %} "
|
||||
"or {% image self.photo max-320x200 as img %}"
|
||||
)
|
||||
|
||||
|
||||
class ImageNode(template.Node):
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import unittest
|
||||
|
||||
from mock import Mock
|
||||
|
||||
from django.utils.six import BytesIO
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailimages import image_operations
|
||||
from wagtail.wagtailimages.exceptions import InvalidFilterSpecError
|
||||
from wagtail.wagtailimages.models import Image, Filter
|
||||
from wagtail.wagtailimages.tests.utils import get_test_image_file
|
||||
|
||||
|
||||
class WillowOperationRecorder(object):
|
||||
|
@ -378,3 +383,28 @@ class TestCacheKey(unittest.TestCase):
|
|||
cache_key = fil.get_cache_key(image)
|
||||
|
||||
self.assertEqual(cache_key, '0bbe3b2f')
|
||||
|
||||
|
||||
class TestFilter(unittest.TestCase):
|
||||
|
||||
operation_instance = Mock()
|
||||
|
||||
def test_runs_operations(self):
|
||||
self.operation_instance.run = Mock()
|
||||
|
||||
fil = Filter(spec='operation1|operation2')
|
||||
image = Image.objects.create(
|
||||
title="Test image",
|
||||
file=get_test_image_file(),
|
||||
)
|
||||
fil.run(image, BytesIO())
|
||||
|
||||
self.assertEqual(self.operation_instance.run.call_count, 2)
|
||||
|
||||
|
||||
@hooks.register('register_image_operations')
|
||||
def register_image_operations():
|
||||
return [
|
||||
('operation1', Mock(return_value=TestFilter.operation_instance)),
|
||||
('operation2', Mock(return_value=TestFilter.operation_instance))
|
||||
]
|
||||
|
|
|
@ -90,6 +90,27 @@ class TestImageTag(TestCase):
|
|||
self.assertTrue('width="400"' in result)
|
||||
self.assertTrue('height="300"' in result)
|
||||
|
||||
def test_image_tag_with_chained_filters(self):
|
||||
result = self.render_image_tag(self.image, 'fill-200x200 height-150')
|
||||
self.assertTrue('width="150"' in result)
|
||||
self.assertTrue('height="150"' in result)
|
||||
|
||||
def test_filter_specs_must_match_allowed_pattern(self):
|
||||
with self.assertRaises(template.TemplateSyntaxError):
|
||||
self.render_image_tag(self.image, 'fill-200x200|height-150')
|
||||
|
||||
with self.assertRaises(template.TemplateSyntaxError):
|
||||
self.render_image_tag(self.image, 'fill-800x600 alt"test"')
|
||||
|
||||
def test_context_may_only_contain_one_argument(self):
|
||||
with self.assertRaises(template.TemplateSyntaxError):
|
||||
temp = template.Template(
|
||||
'{% load wagtailimages_tags %}{% image image_obj fill-200x200'
|
||||
' as test_img this_one_should_not_be_there %}<img {{ test_img.attrs }} />'
|
||||
)
|
||||
context = template.Context({'image_obj': self.image})
|
||||
temp.render(context)
|
||||
|
||||
|
||||
class TestMissingImage(TestCase):
|
||||
"""
|
||||
|
@ -393,6 +414,18 @@ class TestRenditionFilenames(TestCase):
|
|||
|
||||
self.assertEqual(rendition.file.name, 'images/test_rf3.15ee4958.fill-100x100.png')
|
||||
|
||||
def test_filter_with_pipe_gets_dotted(self):
|
||||
image = Image.objects.create(
|
||||
title="Test image",
|
||||
file=get_test_image_file(filename='test_rf4.png'),
|
||||
)
|
||||
image.set_focal_point(Rect(100, 100, 200, 200))
|
||||
image.save()
|
||||
|
||||
rendition = image.get_rendition('fill-200x200|height-150')
|
||||
|
||||
self.assertEqual(rendition.file.name, 'images/test_rf4.15ee4958.fill-200x200.height-150.png')
|
||||
|
||||
|
||||
class TestDifferentUpload(TestCase):
|
||||
def test_upload_path(self):
|
||||
|
|
Ładowanie…
Reference in New Issue