瀏覽代碼

Add "description" field to AbstractImage

- Baseline support for upcoming alt text capabilities
Chiemezuo 1 年之前
父節點
當前提交
5cc22f3f75

+ 1 - 0
CHANGELOG.txt

@@ -18,6 +18,7 @@ Changelog
  * Consistently use `capfirst` for title-casing model verbose names (Sébastien Corbin)
  * Add support for Python 3.13 (Matt Westcott)
  * Fire `copy_for_translation_done` signal when copying translatable models as well as pages (Coen van der Kamp)
+ * Add support for an image `description` field across all images, to better support accessible image descriptions (Chiemezuo Akujobi)
  * Fix: Prevent page type business rules from blocking reordering of pages (Andy Babic, Sage Abdullah)
  * Fix: Improve layout of object permissions table (Sage Abdullah)
  * Fix: Fix typo in aria-label attribute of page explorer navigation link (Sébastien Corbin)

+ 1 - 0
docs/releases/6.3.md

@@ -33,6 +33,7 @@ This release adds formal support for Django 5.1.
  * Deprecate the `WAGTAIL_AUTO_UPDATE_PREVIEW` setting, use `WAGTAIL_AUTO_UPDATE_PREVIEW_INTERVAL = 0` instead (Sage Abdullah)
  * Consistently use `capfirst` for title-casing model verbose names (Sébastien Corbin)
  * Fire `copy_for_translation_done` signal when copying translatable models as well as pages (Coen van der Kamp)
+ * Add support for an image `description` field across all images, to better support accessible image descriptions (Chiemezuo Akujobi)
 
 ### Bug fixes
 

+ 20 - 0
wagtail/images/migrations/0027_image_description.py

@@ -0,0 +1,20 @@
+# Generated by Django 4.2.13 on 2024-08-05 22:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("wagtailimages", "0026_delete_uploadedimage"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="image",
+            name="description",
+            field=models.CharField(
+                blank=True, default="", max_length=255, verbose_name="description"
+            ),
+        ),
+    ]

+ 10 - 3
wagtail/images/models.py

@@ -262,6 +262,12 @@ class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed, models.Mode
         width_field="width",
         height_field="height",
     )
+    description = models.CharField(
+        blank=True,
+        max_length=255,
+        verbose_name=_("description"),
+        default="",
+    )
     width = models.IntegerField(verbose_name=_("width"), editable=False)
     height = models.IntegerField(verbose_name=_("height"), editable=False)
     created_at = models.DateTimeField(
@@ -823,9 +829,9 @@ class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed, models.Mode
     @property
     def default_alt_text(self):
         # by default the alt text field (used in rich text insertion) is populated
-        # from the title. Subclasses might provide a separate alt field, and
-        # override this
-        return self.title
+        # from the description. In the absence of that, it is populated from the title.
+        # Subclasses might provide a separate alt field, and override this
+        return getattr(self, "description", None) or self.title
 
     def is_editable_by_user(self, user):
         from wagtail.images.permissions import permission_policy
@@ -840,6 +846,7 @@ class Image(AbstractImage):
     admin_form_fields = (
         "title",
         "file",
+        "description",
         "collection",
         "tags",
         "focal_point_x",

+ 1 - 0
wagtail/images/templates/wagtailimages/multiple/add.html

@@ -64,6 +64,7 @@
                 </div>
             </div>
             <div class="right col9">
+                <p class="error-message">{% trans "Please provide an image description to comply with best practices for accessibility." %}</p>
                 <p class="status-msg success">{% trans "Upload successful. Please update this image with a more appropriate title, if necessary. You may also delete the image completely if the upload wasn't required." %}</p>
                 <p class="status-msg warning">
                     {% trans "Upload successful. However, your new image seems to be a duplicate of this existing image. You may delete it if it wasn't required." %}

+ 17 - 0
wagtail/images/tests/test_models.py

@@ -145,6 +145,23 @@ class TestImage(TestCase):
         )
         self.assertIsNone(image.get_suggested_focal_point())
 
+    def test_default_with_description(self):
+        # Primary default should be description
+        image = Image.objects.create(
+            title="Test Image",
+            description="This is a test description",
+            file=get_test_image_file(),
+        )
+        self.assertEqual(image.default_alt_text, image.description)
+
+    def test_default_without_description(self):
+        # Secondary default should be title
+        image = Image.objects.create(
+            title="Test Image",
+            file=get_test_image_file(),
+        )
+        self.assertEqual(image.default_alt_text, image.title)
+
 
 class TestImageQuerySet(TransactionTestCase):
     fixtures = ["test_empty.json"]

+ 99 - 95
wagtail/images/tests/test_signal_handlers.py

@@ -1,95 +1,99 @@
-from django.db import transaction
-from django.test import TestCase, TransactionTestCase, override_settings
-
-from wagtail.images import get_image_model, signal_handlers
-from wagtail.images.tests.utils import get_test_image_file
-from wagtail.models import Collection
-
-from .utils import Image
-
-
-class TestFilesDeletedForDefaultModels(TransactionTestCase):
-    """
-    Because we expect file deletion to only happen once a transaction is
-    successfully committed, we must run these tests using TransactionTestCase
-    per the following documentation:
-
-        Django's TestCase class wraps each test in a transaction and rolls back that
-        transaction after each test, in order to provide test isolation. This means
-        that no transaction is ever actually committed, thus your on_commit()
-        callbacks will never be run. If you need to test the results of an
-        on_commit() callback, use a TransactionTestCase instead.
-        https://docs.djangoproject.com/en/1.10/topics/db/transactions/#use-in-tests
-    """
-
-    def setUp(self):
-        # Required to create root collection because the TransactionTestCase
-        # does not make initial data loaded in migrations available and
-        # serialized_rollback=True causes other problems in the test suite.
-        # ref: https://docs.djangoproject.com/en/1.10/topics/testing/overview/#rollback-emulation
-        Collection.objects.get_or_create(
-            name="Root",
-            path="0001",
-            depth=1,
-            numchild=0,
-        )
-
-    def test_image_file_deleted_oncommit(self):
-        with transaction.atomic():
-            image = get_image_model().objects.create(
-                title="Test Image", file=get_test_image_file()
-            )
-            filename = image.file.name
-            self.assertTrue(image.file.storage.exists(filename))
-            image.delete()
-            self.assertTrue(image.file.storage.exists(filename))
-        self.assertFalse(image.file.storage.exists(filename))
-
-    def test_rendition_file_deleted_oncommit(self):
-        with transaction.atomic():
-            image = get_image_model().objects.create(
-                title="Test Image", file=get_test_image_file()
-            )
-            rendition = image.get_rendition("original")
-            filename = rendition.file.name
-            self.assertTrue(rendition.file.storage.exists(filename))
-            rendition.delete()
-            self.assertTrue(rendition.file.storage.exists(filename))
-        self.assertFalse(rendition.file.storage.exists(filename))
-
-
-@override_settings(WAGTAILIMAGES_IMAGE_MODEL="tests.CustomImage")
-class TestFilesDeletedForCustomModels(TestFilesDeletedForDefaultModels):
-    def setUp(self):
-        # Required to create root collection because the TransactionTestCase
-        # does not make initial data loaded in migrations available and
-        # serialized_rollback=True causes other problems in the test suite.
-        # ref: https://docs.djangoproject.com/en/1.10/topics/testing/overview/#rollback-emulation
-        Collection.objects.get_or_create(
-            name="Root",
-            path="0001",
-            depth=1,
-            numchild=0,
-        )
-
-        #: Sadly signal receivers only get connected when starting django.
-        #: We will re-attach them here to mimic the django startup behaviour
-        #: and get the signals connected to our custom model..
-        signal_handlers.register_signal_handlers()
-
-    def test_image_model(self):
-        cls = get_image_model()
-        self.assertEqual(f"{cls._meta.app_label}.{cls.__name__}", "tests.CustomImage")
-
-
-@override_settings(WAGTAILIMAGES_FEATURE_DETECTION_ENABLED=True)
-class TestRawForPreSaveImageFeatureDetection(TestCase):
-    fixtures = ["test.json"]
-
-    # just to test the file is from a fixture doesn't actually exists.
-    # raw check in pre_save_image_feature_detection skips on the provided condition of this test
-    # hence avoiding an error
-
-    def test_image_does_not_exist(self):
-        bad_image = Image.objects.get(pk=1)
-        self.assertFalse(bad_image.file.storage.exists(bad_image.file.name))
+from django.db import transaction
+from django.test import TestCase, TransactionTestCase, override_settings
+
+from wagtail.images import get_image_model, signal_handlers
+from wagtail.images.tests.utils import get_test_image_file
+from wagtail.models import Collection
+
+from .utils import Image
+
+
+class TestFilesDeletedForDefaultModels(TransactionTestCase):
+    """
+    Because we expect file deletion to only happen once a transaction is
+    successfully committed, we must run these tests using TransactionTestCase
+    per the following documentation:
+
+        Django's TestCase class wraps each test in a transaction and rolls back that
+        transaction after each test, in order to provide test isolation. This means
+        that no transaction is ever actually committed, thus your on_commit()
+        callbacks will never be run. If you need to test the results of an
+        on_commit() callback, use a TransactionTestCase instead.
+        https://docs.djangoproject.com/en/1.10/topics/db/transactions/#use-in-tests
+    """
+
+    def setUp(self):
+        # Required to create root collection because the TransactionTestCase
+        # does not make initial data loaded in migrations available and
+        # serialized_rollback=True causes other problems in the test suite.
+        # ref: https://docs.djangoproject.com/en/1.10/topics/testing/overview/#rollback-emulation
+        Collection.objects.get_or_create(
+            name="Root",
+            path="0001",
+            depth=1,
+            numchild=0,
+        )
+
+    def test_image_file_deleted_oncommit(self):
+        with transaction.atomic():
+            image = get_image_model().objects.create(
+                title="Test Image",
+                description="A test description",
+                file=get_test_image_file(),
+            )
+            filename = image.file.name
+            self.assertTrue(image.file.storage.exists(filename))
+            image.delete()
+            self.assertTrue(image.file.storage.exists(filename))
+        self.assertFalse(image.file.storage.exists(filename))
+
+    def test_rendition_file_deleted_oncommit(self):
+        with transaction.atomic():
+            image = get_image_model().objects.create(
+                title="Test Image",
+                description="A test description",
+                file=get_test_image_file(),
+            )
+            rendition = image.get_rendition("original")
+            filename = rendition.file.name
+            self.assertTrue(rendition.file.storage.exists(filename))
+            rendition.delete()
+            self.assertTrue(rendition.file.storage.exists(filename))
+        self.assertFalse(rendition.file.storage.exists(filename))
+
+
+@override_settings(WAGTAILIMAGES_IMAGE_MODEL="tests.CustomImage")
+class TestFilesDeletedForCustomModels(TestFilesDeletedForDefaultModels):
+    def setUp(self):
+        # Required to create root collection because the TransactionTestCase
+        # does not make initial data loaded in migrations available and
+        # serialized_rollback=True causes other problems in the test suite.
+        # ref: https://docs.djangoproject.com/en/1.10/topics/testing/overview/#rollback-emulation
+        Collection.objects.get_or_create(
+            name="Root",
+            path="0001",
+            depth=1,
+            numchild=0,
+        )
+
+        #: Sadly signal receivers only get connected when starting django.
+        #: We will re-attach them here to mimic the django startup behaviour
+        #: and get the signals connected to our custom model..
+        signal_handlers.register_signal_handlers()
+
+    def test_image_model(self):
+        cls = get_image_model()
+        self.assertEqual(f"{cls._meta.app_label}.{cls.__name__}", "tests.CustomImage")
+
+
+@override_settings(WAGTAILIMAGES_FEATURE_DETECTION_ENABLED=True)
+class TestRawForPreSaveImageFeatureDetection(TestCase):
+    fixtures = ["test.json"]
+
+    # just to test the file is from a fixture doesn't actually exists.
+    # raw check in pre_save_image_feature_detection skips on the provided condition of this test
+    # hence avoiding an error
+
+    def test_image_does_not_exist(self):
+        bad_image = Image.objects.get(pk=1)
+        self.assertFalse(bad_image.file.storage.exists(bad_image.file.name))

+ 4 - 0
wagtail/images/tests/tests.py

@@ -722,6 +722,7 @@ class TestGetImageForm(WagtailTestUtils, TestCase):
             [
                 "title",
                 "file",
+                "description",
                 "collection",
                 "tags",
                 "focal_point_x",
@@ -739,6 +740,7 @@ class TestGetImageForm(WagtailTestUtils, TestCase):
             [
                 "title",
                 "file",
+                "description",
                 "collection",
                 "tags",
                 "focal_point_x",
@@ -844,11 +846,13 @@ class TestDifferentUpload(TestCase):
     def test_upload_path(self):
         image = CustomImageFilePath.objects.create(
             title="Test image",
+            description="A test description",
             file=get_test_image_file(),
         )
 
         second_image = CustomImageFilePath.objects.create(
             title="Test Image",
+            description="A test description",
             file=get_test_image_file(colour="black"),
         )
 

+ 41 - 0
wagtail/test/testapp/migrations/0043_customimage_description.py

@@ -0,0 +1,41 @@
+# Generated by Django 5.0.7 on 2024-08-06 07:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("tests", "0042_alter_customdocument_file_size_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="customimage",
+            name="description",
+            field=models.CharField(
+                blank=True, default="", max_length=255, verbose_name="description"
+            ),
+        ),
+        migrations.AddField(
+            model_name="customimagefilepath",
+            name="description",
+            field=models.CharField(
+                blank=True, default="", max_length=255, verbose_name="description"
+            ),
+        ),
+        migrations.AddField(
+            model_name="customimagewithauthor",
+            name="description",
+            field=models.CharField(
+                blank=True, default="", max_length=255, verbose_name="description"
+            ),
+        ),
+        migrations.AddField(
+            model_name="customrestaurantimage",
+            name="description",
+            field=models.CharField(
+                blank=True, default="", max_length=255, verbose_name="description"
+            ),
+        ),
+    ]