diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 47c84eba3b..7c911f4b23 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -90,10 +90,13 @@ class FieldFile(File, AltersData): # to further manipulate the underlying file, as well as update the # associated model instance. + def _set_instance_attribute(self, name, content): + setattr(self.instance, self.field.attname, name) + def save(self, name, content, save=True): name = self.field.generate_filename(self.instance, name) self.name = self.storage.save(name, content, max_length=self.field.max_length) - setattr(self.instance, self.field.attname, self.name) + self._set_instance_attribute(self.name, content) self._committed = True # Save the object because it has changed, unless save is False @@ -391,6 +394,12 @@ class ImageFileDescriptor(FileDescriptor): class ImageFieldFile(ImageFile, FieldFile): + def _set_instance_attribute(self, name, content): + setattr(self.instance, self.field.attname, content) + # Update the name in case generate_filename() or storage.save() changed + # it, but bypass the descriptor to avoid re-reading the file. + self.instance.__dict__[self.field.attname] = self.name + def delete(self, save=True): # Clear the image dimensions cache if hasattr(self, "_dimensions_cache"): diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index d846788815..68315205df 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -437,6 +437,11 @@ Miscellaneous :class:`~django.core.exceptions.FieldError` when saving a file without a ``name``. +* ``ImageField.update_dimension_fields(force=True)`` is no longer called after + saving the image to storage. If your storage backend resizes images, the + ``width_field`` and ``height_field`` will not match the width and height of + the image. + .. _deprecated-features-5.1: Features deprecated in 5.1 diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py index e34f3c8947..18a6fdbc1c 100644 --- a/tests/model_fields/models.py +++ b/tests/model_fields/models.py @@ -13,6 +13,8 @@ from django.db.models.functions import Lower from django.utils.functional import SimpleLazyObject from django.utils.translation import gettext_lazy as _ +from .storage import NoReadFileSystemStorage + try: from PIL import Image except ImportError: @@ -373,6 +375,21 @@ if Image: width_field="headshot_width", ) + class PersonNoReadImage(models.Model): + """ + Model that defines an ImageField with a storage backend that does not + support reading. + """ + + mugshot = models.ImageField( + upload_to="tests", + storage=NoReadFileSystemStorage(), + width_field="mugshot_width", + height_field="mugshot_height", + ) + mugshot_width = models.IntegerField() + mugshot_height = models.IntegerField() + class CustomJSONDecoder(json.JSONDecoder): def __init__(self, object_hook=None, *args, **kwargs): diff --git a/tests/model_fields/storage.py b/tests/model_fields/storage.py new file mode 100644 index 0000000000..9002c12683 --- /dev/null +++ b/tests/model_fields/storage.py @@ -0,0 +1,6 @@ +from django.core.files.storage.filesystem import FileSystemStorage + + +class NoReadFileSystemStorage(FileSystemStorage): + def open(self, *args, **kwargs): + raise AssertionError("This storage class does not support reading.") diff --git a/tests/model_fields/test_imagefield.py b/tests/model_fields/test_imagefield.py index 8c93ed1bdb..7265da598b 100644 --- a/tests/model_fields/test_imagefield.py +++ b/tests/model_fields/test_imagefield.py @@ -18,6 +18,7 @@ if Image: from .models import ( Person, PersonDimensionsFirst, + PersonNoReadImage, PersonTwoImages, PersonWithHeight, PersonWithHeightAndWidth, @@ -30,7 +31,7 @@ else: pass PersonWithHeight = PersonWithHeightAndWidth = PersonDimensionsFirst = Person - PersonTwoImages = Person + PersonTwoImages = PersonNoReadImage = Person class ImageFieldTestMixin(SerializeMixin): @@ -469,3 +470,28 @@ class TwoImageFieldTests(ImageFieldTestMixin, TestCase): # Dimensions were recalculated, and hence file should have opened. self.assertIs(p.mugshot.was_opened, True) self.assertIs(p.headshot.was_opened, True) + + +@skipIf(Image is None, "Pillow is required to test ImageField") +class NoReadTests(ImageFieldTestMixin, TestCase): + def test_width_height_correct_name_mangling_correct(self): + instance1 = PersonNoReadImage() + + instance1.mugshot.save("mug", self.file1) + + self.assertEqual(instance1.mugshot_width, 4) + self.assertEqual(instance1.mugshot_height, 8) + + instance1.save() + + self.assertEqual(instance1.mugshot_width, 4) + self.assertEqual(instance1.mugshot_height, 8) + + instance2 = PersonNoReadImage() + instance2.mugshot.save("mug", self.file1) + instance2.save() + + self.assertNotEqual(instance1.mugshot.name, instance2.mugshot.name) + + self.assertEqual(instance1.mugshot_width, instance2.mugshot_width) + self.assertEqual(instance1.mugshot_height, instance2.mugshot_height)