Le modèle Django ImageField crée un dossier imbriqué chaque fois qu'il télécharge un fichier
Je suis sûr qu'il me manque quelque chose de stupide, mais je suis confus.
J'ai le modèle suivant pour mon profil:
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)
Mais le profile_pics
dossier continue de s'imbriquer, donc ma structure de dossiers commence à ressembler à ceci:

Mes variables dans settings.py semblent normales, je crois:
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
Je pense que le problème avec le dossier d'imbrication provient de ma méthode de sauvegarde dans ma classe Profile, en particulier ceci:
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)
qui est déclenché par mes signaux:
@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()
Pourquoi est-ce que cela imbrique les dossiers?
J'utilise la même méthode de sauvegarde sur les images des articles de blog et là, les dossiers ne s'imbriquent pas.
Qu'est-ce que je rate?
PS: Au cas où cela aiderait, voici 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
Éditer:
Je suis d'ailleurs arrivé à cette solution qui évite le problème principal, même si je ne suis pas sûr de son efficacité:
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)
Réponses
Je parie que vous vouliez adapter ce message à vos besoins. L'auteur n'a pas de problème, car il "ne réutilise pas ce dans quoi il économise".
self.image
a un attribut name
. Lorsque vous vérifiez s'il existe ( if self.image
), il a déjà un nom. Ensuite, à chaque mise à jour, vous continuez à redimensionner une image déjà redimensionnée, ce qui continue également d'ajouter un nom d'image déjà existant au upload_to
chemin, donc à chaque itération, il upload_to
+ self.image.name
. Mais self.image.name
c'est déjà /profile_pics/...
.
Pour résoudre ce problème, ajoutez simplement une is_resized
colonne.
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)
N'oubliez pas de régler is_resized
à False
chaque fois que l'image change.
Remarque: les signaux en général sont une mauvaise pratique. Je ne pense pas non plus que ce soit une bonne idée d'avoir deux signaux qui opèrent sur les mêmes objets.
Si vous en avez vraiment besoin, pensez à les remplacer par un seul signal.
@receiver(post_save, sender=User)
def handle_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
else:
instance.profile.save()
Bien qu'il soit préférable d'exécuter le redimensionnement de la vignette dans votre vue.
Il y a une chose dans django appelée «signaux dupliqués». Cela se produit partout où votre projet importe le module dans lequel vous définissez les signaux, car l'enregistrement des signaux s'exécute autant de fois qu'il est importé.
Peut-être pouvez-vous résoudre votre problème en passant un identifiant unique comme argument dispatch_uid pour identifier votre fonction de récepteur.
from django.core.signals import request_finished
request_finished.connect(my_callback, dispatch_uid="my_unique_identifier")
La source: https://docs.djangoproject.com/en/3.1/topics/signals/