Django model ImageField cria uma pasta aninhada cada vez que carrega o arquivo

Nov 29 2020

Tenho certeza de que estou perdendo algo bobo, mas estou confuso.

Tenho o seguinte modelo para meu 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)

Mas a profile_picspasta continua se aninhando, então minha estrutura de pastas começa a ficar assim:

Minhas variáveis ​​em settings.py parecem normais, eu acredito:

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

Acredito que o problema com a pasta de aninhamento se origina no meu método de salvamento na minha classe de perfil, especificamente este:

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 é acionado por meus sinais:

@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 que isso está aninhando as pastas?

Estou usando o mesmo método de salvar nas imagens das postagens do blog e aí as pastas não se aninham.

o que estou perdendo?

PS: Caso ajude, aqui é 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 acaso, cheguei a esta solução que está evitando o problema principal, embora não tenha certeza de quão eficiente pode 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)

Respostas

2 TomWojcik Nov 30 2020 at 01:13

Aposto que você queria ajustar esta postagem às suas necessidades. O autor não tem problema, porque ele "não está reutilizando o que está economizando".

self.imagetem atributo name. Ao verificar se existe ( if self.image), já tem um nome. Então, a cada atualização, você continua redimensionando a imagem já redimensionada, o que também continua adicionando o nome da imagem já existente ao upload_tocaminho, de modo que a cada iteração ele faz upload_to+ self.image.name. Mas self.image.namejá está /profile_pics/....

Para resolver esse problema, basta adicionar a is_resizedcoluna.

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)

Lembre-se de definir is_resizedpara Falsesempre que a imagem mudar.

Nota lateral, sinais em geral são uma má prática. Também não acho uma boa ideia ter dois sinais que operam nos mesmos objetos.

Se você realmente precisa deles, considere substituí-los por um único sinal.

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

Embora seja melhor se você redimensionar a miniatura em sua visualização.

2 AadeshDhakal Nov 29 2020 at 22:21

Existe uma coisa no django chamada 'sinais duplicados'. Isso ocorre em todos os lugares em que seu projeto importa o módulo onde você define os sinais, pois o registro do sinal é executado tantas vezes quanto é importado.

Talvez você possa resolver seu problema passando um identificador único como o argumento dispatch_uid para identificar sua função de receptor.

from django.core.signals import request_finished

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

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