diff --git a/wagtail/tests/fixtures/test.json b/wagtail/tests/fixtures/test.json index d16affe480..de8b1e27e8 100644 --- a/wagtail/tests/fixtures/test.json +++ b/wagtail/tests/fixtures/test.json @@ -81,7 +81,8 @@ "audience": "public", "location": "The North Pole", "body": "

Chestnuts roasting on an open fire

", - "cost": "Free" + "cost": "Free", + "feed_image": 1 } }, @@ -223,7 +224,7 @@ "date_from": "2015-04-22", "audience": "public", "location": "Ameristralia", - "body": "

come celebrate the independence of Ameristralia

", + "body": "

come celebrate the independence of Ameristralia

", "cost": "Free" } }, @@ -625,5 +626,16 @@ "page": 11, "password": "swordfish" } +}, +{ + "pk": 1, + "model": "wagtailimages.image", + "fields": { + "title": "A missing image", + "file": "original_images/missing.jpg", + "width": 1000, + "height": 1000, + "created_at": "2014-01-01T12:00:00.000Z" + } } ] diff --git a/wagtail/tests/templates/tests/event_page.html b/wagtail/tests/templates/tests/event_page.html index 795b7bf0a7..a3ccaa7181 100644 --- a/wagtail/tests/templates/tests/event_page.html +++ b/wagtail/tests/templates/tests/event_page.html @@ -1,4 +1,4 @@ -{% load wagtailcore_tags %} +{% load wagtailcore_tags wagtailimages_tags %} @@ -8,6 +8,10 @@

{{ self.title }}

Event

+ {% if self.feed_image %} + {% image self.feed_image width-200 class="feed-image" %} + {% endif %} + {{ self.body|richtext }}

Back to events index

diff --git a/wagtail/wagtailimages/formats.py b/wagtail/wagtailimages/formats.py index 3c5644a078..1ad99e1188 100644 --- a/wagtail/wagtailimages/formats.py +++ b/wagtail/wagtailimages/formats.py @@ -1,6 +1,7 @@ from django.utils.html import escape from wagtail.utils.apps import get_app_submodules +from wagtail.wagtailimages.models import SourceImageIOError class Format(object): @@ -25,7 +26,15 @@ class Format(object): ) def image_to_html(self, image, alt_text, extra_attributes=''): - rendition = image.get_rendition(self.filter_spec) + try: + rendition = image.get_rendition(self.filter_spec) + except SourceImageIOError: + # Image file is (probably) missing from /media/original_images - generate a dummy + # rendition so that we just output a broken image, rather than crashing out completely + # during rendering + Rendition = image.renditions.model # pick up any custom Image / Rendition classes that may be in use + rendition = Rendition(image=image, width=0, height=0) + rendition.file.name = 'not-found' if self.classnames: class_attr = 'class="%s" ' % escape(self.classnames) diff --git a/wagtail/wagtailimages/models.py b/wagtail/wagtailimages/models.py index 163dfd6767..9e19a03f46 100644 --- a/wagtail/wagtailimages/models.py +++ b/wagtail/wagtailimages/models.py @@ -1,7 +1,7 @@ import os.path import re -from six import BytesIO +from six import BytesIO, text_type from taggit.managers import TaggableManager @@ -28,6 +28,13 @@ from wagtail.wagtailimages.rect import Rect from wagtail.wagtailadmin.utils import get_object_usage +class SourceImageIOError(IOError): + """ + Custom exception to distinguish IOErrors that were thrown while opening the source image + """ + pass + + def get_upload_to(instance, filename): folder_name = 'original_images' filename = instance.file.field.storage.get_valid_name(filename) @@ -178,7 +185,15 @@ class AbstractImage(models.Model, TagSearchable): # If we have a backend attribute then pass it to process # image - else pass 'default' backend_name = getattr(self, 'backend', 'default') - generated_image = filter.process_image(file_field.file, backend_name=backend_name, focal_point=self.get_focal_point()) + + try: + image_file = file_field.file # triggers a call to self.storage.open, so IOErrors from missing files will be raised at this point + except IOError as e: + # re-throw this as a SourceImageIOError so that calling code can distinguish + # these from IOErrors elsewhere in the process + raise SourceImageIOError(text_type(e)) + + generated_image = filter.process_image(image_file, backend_name=backend_name, focal_point=self.get_focal_point()) # generate new filename derived from old one, inserting the filter spec and focal point key before the extension if self.has_focal_point(): diff --git a/wagtail/wagtailimages/templatetags/wagtailimages_tags.py b/wagtail/wagtailimages/templatetags/wagtailimages_tags.py index 5c72734177..c8f609c6b9 100644 --- a/wagtail/wagtailimages/templatetags/wagtailimages_tags.py +++ b/wagtail/wagtailimages/templatetags/wagtailimages_tags.py @@ -1,6 +1,6 @@ from django import template -from wagtail.wagtailimages.models import Filter +from wagtail.wagtailimages.models import Filter, SourceImageIOError register = template.Library() @@ -53,10 +53,10 @@ class ImageNode(template.Node): try: rendition = image.get_rendition(self.filter) - except IOError: + except SourceImageIOError: # It's fairly routine for people to pull down remote databases to their # local dev versions without retrieving the corresponding image files. - # In such a case, we would get an IOError at the point where we try to + # In such a case, we would get a SourceImageIOError at the point where we try to # create the resized version of a non-existent image. Since this is a # bit catastrophic for a missing image, we'll substitute a dummy # Rendition object so that we just output a broken link instead. diff --git a/wagtail/wagtailimages/tests/tests.py b/wagtail/wagtailimages/tests/tests.py index 1030a3b7ef..6df1dba56b 100644 --- a/wagtail/wagtailimages/tests/tests.py +++ b/wagtail/wagtailimages/tests/tests.py @@ -63,6 +63,25 @@ class TestImageTag(TestCase): self.assertTrue('title="my wonderful title"' in result) +class TestMissingImage(TestCase): + """ + Missing image files in media/original_images should be handled gracefully, to cope with + pulling live databases to a development instance without copying the corresponding image files. + In this case, it's acceptable to render broken images, but not to fail rendering the page outright. + """ + fixtures = ['test.json'] + + def test_image_tag_with_missing_image(self): + # the page /events/christmas/ has a missing image as the feed image + response = self.client.get('/events/christmas/') + self.assertContains(response, 'A missing image', html=True) + + def test_rich_text_with_missing_image(self): + # the page /events/final-event/ has a missing image in the rich text body + response = self.client.get('/events/final-event/') + self.assertContains(response, 'where did my image go?', html=True) + + class TestFormat(TestCase): def setUp(self): # test format