Динамический путь к файлу в Django

Я пытаюсь генерировать динамические пути к файлам в Django. Я хочу сделать файловую систему такой:

 -- user_12
     --- photo_1
     --- photo_2
 --- user_ 13
     ---- photo_1

Я нашел связанный вопрос: Django Пользовательское поле загрузки изображения с динамическим путем

Здесь говорят, что мы можем изменить путь upload_to и приводит к https://docs.djangoproject.com/en/stable/topics/files/ doc. В документации есть пример:

from django.db import models
from django.core.files.storage import FileSystemStorage

fs = FileSystemStorage(location='/media/photos')

class Car(models.Model):
    ...
    photo = models.ImageField(storage=fs)

Но, тем не менее, это не динамично, я хочу присвоить идентификатору автомобиля имя изображения, и я не могу присвоить идентификатор до завершения определения автомобиля. Так как я могу создать путь с идентификатором автомобиля?

10 ответов

Решение

Вы можете использовать вызываемый в upload_to аргумент, а не использование пользовательского хранилища. См. Документы и обратите внимание на предупреждение, что первичный ключ еще не может быть установлен при вызове функции (потому что выгрузка может быть обработана до сохранения объекта в базе данных), поэтому используйте ID может быть не возможно Возможно, вы захотите рассмотреть использование другого поля в модели, например, слаг. Например:

import os
def get_upload_path(instance, filename):
    return os.path.join(
      "user_%d" % instance.owner.id, "car_%s" % instance.slug, filename)

затем:

photo = models.ImageField(upload_to=get_upload_path)

https://docs.djangoproject.com/en/stable/ref/models/fields/

def upload_path_handler(instance, filename):
    return "user_{id}/{file}".format(id=instance.user.id, file=filename)

class Car(models.Model):
    ...
    photo = models.ImageField(upload_to=upload_path_handler, storage=fs)

В документах есть предупреждение, но оно не должно повлиять на вас, так как мы User ID а не Car Я БЫ.

В большинстве случаев этот объект еще не был сохранен в базе данных, поэтому, если он использует AutoField по умолчанию, он может еще не иметь значения для своего поля первичного ключа.

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

logo = models.ImageField(upload_to=lambda instance, filename: 'directory/images/{0}/{1}'.format(instance.owner.id, filename))

Мое решение не элегантно, но оно работает:

В модели используйте стандартную функцию, для которой потребуется id/pk

def directory_path(instance, filename):
    return 'files/instance_id_{0}/{1}'.format(instance.pk, filename)

в views.py сохраните форму следующим образом:

f=form.save(commit=False)
ftemp1=f.filefield
f.filefield=None
f.save()
#And now that we have crated the record we can add it
f.filefield=ftemp1
f.save()

Это сработало для меня. Примечание: мое файловое поле в моделях и допускается для значений Null. Null=True

Ну, очень поздно на вечеринку, но этот работает для меня.

def content_file_name(instance, filename):
    upload_dir = os.path.join('uploads',instance.albumname)
    if not os.path.exists(upload_dir):
        os.makedirs(upload_dir)
    return os.path.join(upload_dir, filename)

Модель как эта только

class Album(models.Model):
    albumname = models.CharField(max_length=100)
    audiofile = models.FileField(upload_to=content_file_name)

На DjangoSnippets есть два решения

  1. Двухэтапное сохранение: https://djangosnippets.org/snippets/1129/
  2. Предварительная загрузка идентификатора (только для PostgreSQL): https://djangosnippets.org/snippets/2731/

Вы можете переопределить модель save метод:

def save_image(instance, filename):
    instance_id = f'{instance.id:03d}'  # 001
    return f'{instance_id}-{filename.lower()}'  # 001-foo.jpg

class Resource(models.Model):
    photo = models.ImageField(upload_to=save_image)

   def save(self, *args, **kwargs):
        if self.id is None:
            photo = self.photo
            self.photo = None
            super().save(*args, **kwargs)
            self.photo = photo
            if 'force_insert' in kwargs:
                kwargs.pop('force_insert')
        super().save(*args, **kwargs)

Поскольку первичный ключ (id) может быть недоступен, если экземпляр модели еще не был сохранен в базе данных, я написал свои подклассы FileField, которые перемещают файл при сохранении модели, и подкласс хранения, который удаляет старые файлы.

Место хранения:

class OverwriteFileSystemStorage(FileSystemStorage):
    def _save(self, name, content):
        self.delete(name)
        return super()._save(name, content)

    def get_available_name(self, name):
        return name

    def delete(self, name):
        super().delete(name)

        last_dir = os.path.dirname(self.path(name))

        while True:
            try:
                os.rmdir(last_dir)
            except OSError as e:
                if e.errno in {errno.ENOTEMPTY, errno.ENOENT}:
                    break

                raise e

            last_dir = os.path.dirname(last_dir)

FileField:

def tweak_field_save(cls, field):
    field_defined_in_this_class = field.name in cls.__dict__ and field.name not in cls.__bases__[0].__dict__

    if field_defined_in_this_class:
        orig_save = cls.save

        if orig_save and callable(orig_save):
            assert isinstance(field.storage, OverwriteFileSystemStorage), "Using other storage than '{0}' may cause unexpected behavior.".format(OverwriteFileSystemStorage.__name__)

            def save(self, *args, **kwargs):
                if self.pk is None:
                    orig_save(self, *args, **kwargs)

                    field_file = getattr(self, field.name)

                    if field_file:
                        old_path = field_file.path
                        new_filename = field.generate_filename(self, os.path.basename(old_path))
                        new_path = field.storage.path(new_filename)
                        os.makedirs(os.path.dirname(new_path), exist_ok=True)
                        os.rename(old_path, new_path)
                        setattr(self, field.name, new_filename)

                    # for next save
                    if len(args) > 0:
                        args = tuple(v if k >= 2 else False for k, v in enumerate(args))

                    kwargs['force_insert'] = False
                    kwargs['force_update'] = False

                orig_save(self, *args, **kwargs)

            cls.save = save


def tweak_field_class(orig_cls):
    orig_init = orig_cls.__init__

    def __init__(self, *args, **kwargs):
        if 'storage' not in kwargs:
            kwargs['storage'] = OverwriteFileSystemStorage()

        if orig_init and callable(orig_init):
            orig_init(self, *args, **kwargs)

    orig_cls.__init__ = __init__

    orig_contribute_to_class = orig_cls.contribute_to_class

    def contribute_to_class(self, cls, name):
        if orig_contribute_to_class and callable(orig_contribute_to_class):
            orig_contribute_to_class(self, cls, name)

        tweak_field_save(cls, self)

    orig_cls.contribute_to_class = contribute_to_class

    return orig_cls


def tweak_file_class(orig_cls):
    """
    Overriding FieldFile.save method to remove the old associated file.
    I'm doing the same thing in OverwriteFileSystemStorage, but it works just when the names match.
    I probably want to preserve both methods if anyone calls Storage.save.
    """

    orig_save = orig_cls.save

    def new_save(self, name, content, save=True):
        self.delete(save=False)

        if orig_save and callable(orig_save):
            orig_save(self, name, content, save=save)

    new_save.__name__ = 'save'
    orig_cls.save = new_save

    return orig_cls


@tweak_file_class
class OverwriteFieldFile(models.FileField.attr_class):
    pass


@tweak_file_class
class OverwriteImageFieldFile(models.ImageField.attr_class):
    pass


@tweak_field_class
class RenamedFileField(models.FileField):
    attr_class = OverwriteFieldFile


@tweak_field_class
class RenamedImageField(models.ImageField):
    attr_class = OverwriteImageFieldFile

и мои загружаемые файлы выглядят так:

def user_image_path(instance, filename):
    name, ext = 'image', os.path.splitext(filename)[1]

    if instance.pk is not None:
        return os.path.join('users', os.path.join(str(instance.pk), name + ext))

    return os.path.join('users', '{0}_{1}{2}'.format(uuid1(), name, ext))
MEDIA_ROOT/
   /company_Company1/company.png
                    /shop_Shop1/shop.png
                               /bikes/bike.png


def photo_path_company(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/
    return 'company_{0}/{1}'.format(instance.name, filename)

class Company(models.Model):
    name = models.CharField()
    photo = models.ImageField(max_length=255, upload_to=photo_path_company)

def photo_path_shop(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/shop_<name>/
    parent_path = instance.company._meta.get_field('photo').upload_to(instance.company, '')
    return parent_path + 'shop_{0}/{1}'.format(instance.name, filename)

class Shop(models.Model):
    name = models.CharField()
    photo = models.ImageField(max_length=255, upload_to=photo_path_shop)

def photo_path_bike(instance, filename):
    # file will be uploaded to MEDIA_ROOT/company_<name>/shop_<name>/bikes/
    parent_path = instance.shop._meta.get_field('photo').upload_to(instance.shop, '')
    return parent_path + 'bikes/{0}'.format(filename)


class Bike(models.Model):
    name = models.CharField()
    photo = models.ImageField(max_length=255, upload_to=photo_path_bike)

У этого парня есть способ сделать динамический путь. Идея состоит в том, чтобы установить ваше любимое хранилище и настроить параметр upload_to() с помощью функции.

Надеюсь это поможет.

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

model.py

class dummyexperiment(models.Model):
  def __unicode__(self):
    return str(self.id)

class experiment(models.Model):
  def get_exfile_path(instance, filename):
    if instance.id == None:
      iid = instance.dummye.id
    else:
      iid = instance.id
    return os.path.join('experiments', str(iid), filename)

  exfile = models.FileField(upload_to=get_exfile_path)

  def save(self, *args, **kwargs):
    if self.id == None:
      self.dummye = dummyexperiment()
      self.dummye.save()
    super(experiment, self).save(*args, **kwargs)

Я очень новичок в Python и в Django, но мне кажется, что все в порядке.

другое решение:

def get_theme_path(instance, filename):
  id = instance.id
  if id == None:
    id = max(map(lambda a:a.id,Theme.objects.all())) + 1
  return os.path.join('experiments', str(id), filename)

Метод будет

def user_directory_path(имя_поля):

def upload_path(instance, filename):

    year = datetime.now().year

    name, ext = instance.user, os.path.splitext(filename)[1]

    return f'photos/{year}/{instance._meta.model_name}s/{instance.user}/{field_name}_{name}{ext}'

return upload_path

И в ваших моделях вы можете иметь столько ImageField, сколько захотите. пример

photo = models.ImageField(upload_to=user_directory_path('photo'), null=True, blank=True,)

Passport_photo = models.ImageField(upload_to=user_directory_path('Passport_photo'), null=True, blank=True,)

Другие вопросы по тегам