ImageField в модели Django создает вложенную папку каждый раз, когда загружает файл

Nov 29 2020

Я уверен, что упустил что-то глупое, но я запутался.

В моем профиле используется следующая модель:

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)

Но profile_picsпапка продолжает вложение, поэтому моя структура папок начинает выглядеть так:

Мои переменные в settings.py выглядят нормально, я считаю:

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

Я считаю, что проблема с папкой вложенности связана с моим методом сохранения в моем классе профиля, а именно:

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)

который запускается моими сигналами:

@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()

Почему это вложение папок?

Я использую тот же метод сохранения для изображений сообщений в блоге, и там папки не вкладываются.

Что мне не хватает?

PS: Если это поможет, это 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

Редактировать:

Я случайно пришел к этому решению, которое позволяет избежать основной проблемы, хотя я не уверен, насколько оно может быть эффективным:

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)

Ответы

2 TomWojcik Nov 30 2020 at 01:13

Бьюсь об заклад, вы хотели изменить этот пост под свои нужды. У автора нет проблем, потому что он «не использует повторно то, во что сохраняет».

self.imageимеет атрибут name. Когда вы проверяете, существует ли он ( if self.image), у него уже есть имя. Затем с каждым обновлением вы продолжаете изменять размер уже измененного изображения, которое также продолжает добавлять уже существующее имя изображения в upload_toпуть, поэтому с каждой итерацией он делает upload_to+ self.image.name. Но self.image.nameуже есть /profile_pics/....

Чтобы решить эту проблему, просто добавьте is_resizedстолбец.

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)

Просто не забудьте установить is_resizedзначение Falseпри каждом изменении изображения.

Примечание: сигналы в целом - плохая практика. Я также не считаю хорошей идеей иметь два сигнала, воздействующих на одни и те же объекты.

Если они вам действительно нужны, подумайте о замене их одним сигналом.

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

Хотя было бы лучше, если бы вы изменили размер эскиза в своем представлении.

2 AadeshDhakal Nov 29 2020 at 22:21

В django есть такая вещь, как «повторяющиеся сигналы». Это происходит везде, где ваш проект импортирует модуль, в котором вы определяете сигналы, потому что регистрация сигнала выполняется столько раз, сколько импортировано.

Возможно, вы сможете решить свою проблему, передав уникальный идентификатор в качестве аргумента dispatch_uid, чтобы идентифицировать функцию получателя.

from django.core.signals import request_finished

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

Источник: https://docs.djangoproject.com/en/3.1/topics/signals/