diff --git a/pictures/migrations.py b/pictures/migrations.py index 9fe6eba..fe0ebff 100644 --- a/pictures/migrations.py +++ b/pictures/migrations.py @@ -56,7 +56,9 @@ def update_pictures(self, from_field: PictureField, to_model: type[models.Model] new_field_file.update_all(old_field_file) def from_picture_field(self, from_model: type[models.Model]): - for obj in from_model._default_manager.all().iterator(): + for obj in from_model._default_manager.exclude( + Q(**{self.name: ""}) | Q(**{self.name: None}) + ).iterator(): field_file = getattr(obj, self.name) field_file.delete_all() @@ -66,9 +68,13 @@ def to_picture_field( from_field = from_model._meta.get_field(self.name) if hasattr(from_field.attr_class, "delete_variations"): # remove obsolete django-stdimage variations - for obj in from_model._default_manager.all().iterator(): + for obj in from_model._default_manager.exclude( + Q(**{self.name: ""}) | Q(**{self.name: None}) + ).iterator(): field_file = getattr(obj, self.name) field_file.delete_variations() - for obj in to_model._default_manager.all().iterator(): + for obj in to_model._default_manager.exclude( + Q(**{self.name: ""}) | Q(**{self.name: None}) + ).iterator(): field_file = getattr(obj, self.name) field_file.save_all() diff --git a/tests/test_migrations.py b/tests/test_migrations.py index acf7f30..1263d3c 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -171,6 +171,54 @@ class Meta: assert not luke.picture + @pytest.mark.django_db + def test_update_pictures__with_empty_pictures( + self, request, stub_worker, image_upload_file + ): + """Test that update_pictures skips objects with empty/null pictures.""" + + class ToModel(models.Model): + name = models.CharField(max_length=100) + picture = PictureField( + upload_to="testapp/profile/", aspect_ratios=[None, "21/9"], blank=True + ) + + class Meta: + app_label = request.node.name + db_table = "testapp_profile" + + # Create profiles with different picture states + luke = Profile.objects.create(name="Luke", picture=image_upload_file) + leia = Profile.objects.create(name="Leia", picture="") + han = Profile.objects.create(name="Han", picture=None) + stub_worker.join() + + path = luke.picture.aspect_ratios["16/9"]["AVIF"][100].path + assert path.exists() + + migration = migrations.AlterPictureField("profile", "picture", PictureField()) + from_field = Profile._meta.get_field("picture") + + # This should not fail despite empty/null pictures + migration.update_pictures(from_field, ToModel) + stub_worker.join() + + # Verify old path was deleted and new one was created for luke + assert not path.exists() + luke.refresh_from_db() + path = ( + ToModel.objects.get(pk=luke.pk) + .picture.aspect_ratios["21/9"]["AVIF"][100] + .path + ) + assert path.exists() + + # Verify empty profiles still exist and remain empty + leia_profile = Profile.objects.get(pk=leia.pk) + assert not leia_profile.picture + han_profile = Profile.objects.get(pk=han.pk) + assert not han_profile.picture + @pytest.mark.django_db def test_from_picture_field(self, stub_worker, image_upload_file): luke = Profile.objects.create(name="Luke", picture=image_upload_file) @@ -182,6 +230,30 @@ def test_from_picture_field(self, stub_worker, image_upload_file): stub_worker.join() assert not path.exists() + @pytest.mark.django_db + def test_from_picture_field__with_empty_pictures( + self, stub_worker, image_upload_file + ): + """Test that from_picture_field skips objects with empty/null pictures.""" + # Create profiles with different picture states + luke = Profile.objects.create(name="Luke", picture=image_upload_file) + Profile.objects.create(name="Leia", picture="") + Profile.objects.create(name="Han", picture=None) + stub_worker.join() + + path = luke.picture.aspect_ratios["16/9"]["AVIF"][100].path + assert path.exists() + + migration = migrations.AlterPictureField("profile", "picture", PictureField()) + # This should not fail despite empty/null pictures + migration.from_picture_field(Profile) + stub_worker.join() + + assert not path.exists() + # Verify other profiles still exist and weren't affected + assert Profile.objects.filter(name="Leia").exists() + assert Profile.objects.filter(name="Han").exists() + @pytest.mark.django_db def test_to_picture_field(self, request, stub_worker, image_upload_file): class FromModel(models.Model): @@ -234,6 +306,53 @@ class Meta: migration = migrations.AlterPictureField("profile", "picture", PictureField()) migration.to_picture_field(FromModel, Profile) + @pytest.mark.django_db + def test_to_picture_field__with_empty_pictures( + self, request, stub_worker, image_upload_file + ): + """Test that to_picture_field skips objects with empty/null pictures.""" + + class FromModel(models.Model): + picture = models.ImageField(blank=True) + + class Meta: + app_label = request.node.name + db_table = "testapp_profile" + + class ToModel(models.Model): + name = models.CharField(max_length=100) + picture = models.ImageField(upload_to="testapp/profile/", blank=True) + + class Meta: + app_label = request.node.name + db_table = "testapp_profile" + + # Create profiles with different picture states + luke = ToModel.objects.create(name="Luke", picture=image_upload_file) + leia = ToModel.objects.create(name="Leia", picture="") + han = ToModel.objects.create(name="Han", picture=None) + stub_worker.join() + + migration = migrations.AlterPictureField("profile", "picture", PictureField()) + # This should not fail despite empty/null pictures + migration.to_picture_field(FromModel, Profile) + stub_worker.join() + + luke.refresh_from_db() + # Verify only luke's picture was processed + path = ( + Profile.objects.get(pk=luke.pk) + .picture.aspect_ratios["16/9"]["AVIF"][100] + .path + ) + assert path.exists() + + # Verify empty profiles still exist + leia_profile = Profile.objects.get(pk=leia.pk) + assert not leia_profile.picture + han_profile = Profile.objects.get(pk=han.pk) + assert not han_profile.picture + @pytest.mark.django_db def test_to_picture_field__from_stdimage( self, request, stub_worker, image_upload_file