El modelo ImageField de Django crea una carpeta anidada cada vez que carga un archivo

Nov 29 2020

Estoy seguro de que me estoy perdiendo algo tonto, pero estoy confundido.

Tengo el siguiente modelo para mi perfil:

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    image = models.ImageField(
        default="default.jpg",
        upload_to="profile_pics/",
        validators=[FileExtensionValidator(["jpeg", "jpg", "png"])],
    )

    def __str__(self):
        return f"{self.user.username} Profile"

    def save(self, *args, **kwargs):
        if self.image:
            self.image = make_thumbnail(self.image, size=(200, 200))
            super().save(*args, **kwargs)
        else:
            super().save(*args, **kwargs)

Pero la profile_picscarpeta sigue anidando, por lo que la estructura de mi carpeta comienza a verse así:

Mis variables en settings.py parecen normales, creo:

BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"

Creo que el problema con la carpeta de anidamiento se origina con mi método de guardado en mi clase de perfil, específicamente esto:

def save(self, *args, **kwargs):
    if self.image:
        self.image = make_thumbnail(self.image, size=(200, 200))
        super().save(*args, **kwargs)
    else:
        super().save(*args, **kwargs)

que se activa por mis señales:

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)


@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
    instance.profile.save()

¿Por qué esto anida las carpetas?

Estoy usando el mismo método de guardado en las imágenes de las publicaciones del blog y allí las carpetas no se anidan.

¿Qué me estoy perdiendo?

PD: En caso de que ayude, este es make_thumbnail:

from io import BytesIO
from django.core.files import File
from PIL import Image

def make_thumbnail(image, size=(600, 600)):
    im = Image.open(image)
    if im.format == "JPEG":
        im.convert("RGB")
        im.thumbnail(size)
        thumb_io = BytesIO()
        im.save(thumb_io, "JPEG", quality=85)
        image = File(thumb_io, name=image.name)
    else:
        im.convert("RGBA")
        im.thumbnail(size)
        thumb_io = BytesIO()
        im.save(thumb_io, "PNG", quality=85)
        image = File(thumb_io, name=image.name)
    return image

Editar:

Por cierto, llegué a esta solución que es evitar el problema principal, aunque no estoy seguro de cuán eficiente puede ser:

def save(self, *args, **kwargs):
    if self.image:
        self.image = make_thumbnail(self.image, size=(200, 200))
        image_name = self.image.name
        ext = image_name.split(".")[-1]
        filename = "%s.%s" % (uuid.uuid4(), ext)
        clean_name = os.path.join("", filename)
        self.image.name = clean_name
        super().save(*args, **kwargs)
    else:
        super().save(*args, **kwargs)

Respuestas

2 TomWojcik Nov 30 2020 at 01:13

Apuesto a que querías ajustar esta publicación a tus necesidades. El autor no tiene ningún problema, porque "no está reutilizando lo que está guardando".

self.imagetiene atributo name. Cuando verificas si existe ( if self.image), ya tiene un nombre. Luego, con cada actualización, sigue cambiando el tamaño de la imagen ya redimensionada, que también sigue agregando el nombre de la imagen ya existente a la upload_toruta, por lo que con cada iteración lo hace upload_to+ self.image.name. Pero self.image.nameya lo es /profile_pics/....

Para resolver este problema, simplemente agregue is_resizedcolumn.

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    image = models.ImageField(
        default="default.jpg",
        upload_to="profile_pics/",
        validators=[FileExtensionValidator(["jpeg", "jpg", "png"])],
    )
    is_resized = models.BooleanField(default=False)

    def __str__(self):
        return f"{self.user.username} Profile"

    def save(self, *args, **kwargs):
        if self.image and not self.is_resized:
            self.is_resized = True
            self.image = make_thumbnail(self.image, size=(200, 200))
        super().save(*args, **kwargs)

Sólo recuerde conjunto is_resizedpara Falsesiempre la imagen cambia.

Nota al margen, las señales en general son una mala práctica. Tampoco creo que sea una buena idea tener dos señales que operen en los mismos objetos.

Si realmente necesita tenerlos, considere reemplazarlos con una sola señal.

@receiver(post_save, sender=User)
def handle_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)
    else:
        instance.profile.save()

Aunque sería mejor si ejecuta para cambiar el tamaño de la miniatura en su vista.

2 AadeshDhakal Nov 29 2020 at 22:21

Hay una cosa en django llamada 'señales duplicadas'. Esto ocurre en todos los lugares en que su proyecto importa el módulo donde define las señales, porque el registro de señales se ejecuta tantas veces como se importa.

Tal vez pueda resolver su problema pasando un identificador único como argumento dispatch_uid para identificar su función de receptor.

from django.core.signals import request_finished

request_finished.connect(my_callback, dispatch_uid="my_unique_identifier")

Fuente: https://docs.djangoproject.com/en/3.1/topics/signals/