使用当前模型 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 管理文件的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 ImageField 模型调整使用 Django 上传的图像文件的大小? [关闭]