使用当前模型 ID 上传 Django 管理文件

Posted

技术标签:

【中文标题】使用当前模型 ID 上传 Django 管理文件【英文标题】:Django admin file upload with current model id 【发布时间】:2012-04-15 15:32:01 【问题描述】:

我正在尝试使用默认的 Django 管理员创建一个简单的照片库。我想为每个画廊保存一张示例照片,但我不想保留文件名。而不是文件名,我想保存模型的 id (N.jpg)。但是我第一次想保存对象的 id 不存在。我怎么知道模型中的下一个自动增量,或者以某种方式在使用super.save 上传之前和在self.id 存在时上传文件之后保存模型数据?有没有很酷的解决方案?

类似这样的:

def upload_path_handler(instance, filename):
    ext = filename extension
    return "site_media/images/gallery/id.ext".format(id=instance.nextincrement, ext=ext)

class Gallery(models.Model):
    name  = models.TextField()
    image = models.FileField(upload_to=upload_path_handler)

并且可能将文件名存储在不同的字段中。

【问题讨论】:

为什么这值得一票否决?这当然是一个比某些质量更好的问题。 没有可靠的方法可以提前知道下一条记录的id。您可以在最初创建记录后获取 id,但这也受竞争条件的影响。我的建议 - 选择除了 id 之外的其他东西来命名你的文件。 例如当前时间戳+微秒 【参考方案1】:

我遇到了同样的问题。 Okm 的回答让我走上了正确的道路,但在我看来,只需覆盖模型的 save() 方法即可获得相同的功能。

def save(self, *args, **kwargs):
    if self.pk is None:
        saved_image = self.image
        self.image = None
        super(Material, self).save(*args, **kwargs)
        self.image = saved_image

    super(Material, self).save(*args, **kwargs)

这绝对可以正确保存信息。

【讨论】:

这在 django 1.7 中被破坏了吗? 不错的解决方案!过了一会儿,我注意到它在单元测试中中断,因为 kwargs 包含 'force_insert=True' 并且第二次保存导致 IntegrityError: (1062, "Duplicate entry")。在 if 块末尾添加 kwargs.pop('force_insert') 即可解决问题。 @Louis,您可以通过将update_fields=['image'] 指定为参数,仅传入您希望在第二次调用save 时更新的字段名称。 完美的 Pythonic 解决方案!在 Django 2.1.5 中工作。谢谢!【参考方案2】:

图像文件在 Gallery 实例之前保存。因此,您必须通过使用带有 Gallery 实例本身携带状态的信号将保存分为两个阶段:

from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver

_UNSAVED_FILEFIELD = 'unsaved_filefield'

@receiver(pre_save, sender=Image)
def skip_saving_file(sender, instance, **kwargs):
    if not instance.pk and not hasattr(instance, _UNSAVED_FILEFIELD):
        setattr(instance, _UNSAVED_FILEFIELD, instance.image)
        instance.image = None

@receiver(post_save, sender=Image)
def save_file(sender, instance, created, **kwargs):
    if created and hasattr(instance, _UNSAVED_FILEFIELD):
        instance.image = getattr(instance, _UNSAVED_FILEFIELD)
        instance.save()        
        # delete it if you feel uncomfortable...
        # instance.__dict__.pop(_UNSAVED_FILEFIELD)

upload_path_handler 看起来像

def upload_path_handler(instance, filename):
    import os.path
    fn, ext = os.path.splitext(filename)
    return "site_media/images/gallery/idext".format(id=instance.pk, ext=ext)

如果该字段仅用于图像上传,我建议使用 ImageField 而不是 FileField 进行类型检查。此外,您可能希望规范化文件扩展名(由于 mimetype,这是不必要的),例如

def normalize_ext(image_field):
    try:
        from PIL import Image
    except ImportError:
        import Image
    ext = Image.open(image_field).format
    if hasattr(image_field, 'seek') and callable(image_field.seek):
       image_field.seek(0)
    ext = ext.lower()
    if ext == 'jpeg':
        ext = 'jpg'
    return '.' + ext

【讨论】:

非常感谢! :) 我唯一的评论是:sender=Image 是模型对象的对象,如果其他人会尝试使用此解决方案。 @KBalazs 很高兴它有帮助,只需修复代码,请检查编辑【参考方案3】:

对于 Django 2.2,请遵循以下代码。

def save(self, *args, **kwargs):
    if self.pk is None:
        saved_image = self.image
        self.image = None
        super(Gallery, self).save(*args, **kwargs)
        self.image = saved_image
        if 'force_insert' in kwargs:
            kwargs.pop('force_insert')

    super(Gallery, self).save(*args, **kwargs)

将上面的代码 sn-p 添加到您的“类库”中。

P.S.:当您通过 views.py 保存时,这也适用于 DRF。 请注意,DRF 需要第二个 if(条件)。

【讨论】:

你可以用kwargs.pop('force_insert', None)保存一行。另外,Django 2.2 是针对 Python 3 的,所以你可以写super().save(*args, **kwargs)【参考方案4】:

使用Louis's answer,这里是处理模型中所有FileField 的方法:

class MyModel(models.Model):

    file_field = models.FileField(upload_to=upload_to, blank=True, null=True)

    def save(self, *args, **kwargs):
        if self.id is None:
            saved = []
            for f in self.__class__._meta.get_fields():
                if isinstance(f, models.FileField):
                    saved.append((f.name, getattr(self, f.name)))
                    setattr(self, f.name, None)

            super(self.__class__, self).save(*args, **kwargs)

            for name, val in saved:
                setattr(self, name, val)
        super(self.__class__, self).save(*args, **kwargs)

【讨论】:

【参考方案5】:

在 django 1.7 中,建议的解决方案似乎对我不起作用,所以我编写了 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)

文件字段:

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

我的 upload_to 可调用对象如下所示:

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_12'.format(uuid1(), name, ext))

【讨论】:

以上是关于使用当前模型 ID 上传 Django 管理文件的主要内容,如果未能解决你的问题,请参考以下文章

使用文件字段编辑 Django 模型而不重新上传文件

使用 Django 模型表单上传图像

如何使用 ImageField 模型调整使用 Django 上传的图像文件的大小? [关闭]

django 在模型自定义保存中访问上传的文件,最佳实践?

如何使用ImageField模型调整使用Django上传的图像文件的大小? [关闭]

Django:保存模型时填充用户 ID