我的应用程序需要保存上传图像的多个版本。一张高质量图像和另一张仅用于缩略图使用(低质量)。
目前这大部分时间都在工作,但有时保存方法只是失败并且我的所有缩略图图像都被删除,特别是如果我在表单中使用 remove_cover 复选框
raise ValueError("The '%s' attribute has no file associated with it." % self.field.name) app | ValueError: The 'postcover_tn' attribute has no file associated with it.
-> 在此处查看完整跟踪:https://pastebin.com/hgieMGet
模型.py
class Post(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField()
content = models.TextField(blank=False)
postcover = models.ImageField(
verbose_name="Post Cover",
blank=True,
null=True,
upload_to=image_uploads,
)
postcover_tn = models.ImageField(
verbose_name="Post Cover Thumbnail",
blank=True,
null=True,
upload_to=image_uploads,
)
published_date = models.DateTimeField(auto_now_add=True, null=True)
def save(self, *args, **kwargs):
super(Post, self).save(*args, **kwargs)
if self.postcover:
if not (self.postcover_tn and os.path.exists(self.postcover_tn.path)):
image = Image.open(self.postcover)
outputIoStream = BytesIO()
baseheight = 500
hpercent = baseheight / image.size[1]
wsize = int(image.size[0] * hpercent)
imageTemproaryResized = image.resize((wsize, baseheight))
imageTemproaryResized.save(outputIoStream, format='PNG')
outputIoStream.seek(0)
self.postcover = InMemoryUploadedFile(outputIoStream, 'ImageField',
"%s.png" % self.postcover.name.split('.')[0], 'image/png',
sys.getsizeof(outputIoStream), None)
image = Image.open(self.postcover)
outputIoStream = BytesIO()
baseheight = 175
hpercent = baseheight / image.size[1]
wsize = int(image.size[0] * hpercent)
imageTemproaryResized = image.resize((wsize, baseheight))
imageTemproaryResized.save(outputIoStream, format='PNG')
outputIoStream.seek(0)
self.postcover_tn = InMemoryUploadedFile(outputIoStream, 'ImageField',
"%s.png" % self.postcover.name.split('.')[0], 'image/png',
sys.getsizeof(outputIoStream), None)
elif self.postcover_tn:
self.postcover_tn.delete()
super(Post, self).save(*args, **kwargs)
我似乎也无法正确解决:表格.py:
def save(self, commit=True):
instance = super(PostForm, self).save(commit=False)
if self.cleaned_data.get('remove_cover'):
try:
os.unlink(instance.postcover.path)
except OSError:
pass
instance.postcover = None
if commit:
instance.save()
return instance
最佳答案
也许如果我们从另一个角度看问题,我们可以以其他方式解决它,开箱即用。 signals
在处理图像(添加、更新和删除)以及我如何设法解决您的问题时非常方便:
在 models.py
:
# from django.template.defaultfilters import slugify
class Post(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField()
# slug = models.SlugField('slug', max_length=255,
# unique=True, null=True, blank=True,
# help_text='If blank, the slug will be generated automatically from the given title.'
# )
content = models.TextField(blank=False)
# ------------------------------------------------------------------------------------
# rename images with the current post id/pk (which is UUID) and keep the extension
# for cover thumbnail we append "_thumbnail" to the name
# e.g:
# img/posts/77b122a3d241461b80c51adc41d719fb.jpg
# img/posts/77b122a3d241461b80c51adc41d719fb_thumbnail.jpg
def upload_cover(instance, filename):
ext = filename.split('.')[-1]
filename = '{}.{}'.format(instance.id, ext)
path = 'img/posts/'
return '{}{}'.format(path, filename)
postcover = models.ImageField('Post Cover',
upload_to=upload_cover, # callback function
null=True, blank=True,
help_text=_('Upload Post Cover.')
)
def upload_thumbnail(instance, filename):
ext = filename.split('.')[-1]
filename = '{}_thumbnail.{}'.format(instance.id, ext)
path = 'img/posts/'
return '{}{}'.format(path, filename)
postcover_tn = models.ImageField('Post Cover Thumbnail',
upload_to=upload_thumbnail, # callback function
null=True, blank=True,
help_text=_('Upload Post Cover Thumbnail.')
)
# ------------------------------------------------------------------------------------
published_date = models.DateTimeField(auto_now_add=True, null=True)
def save(self, *args, **kwargs):
# i moved the logic to signals
# if not self.slug:
# self.slug = slugify(self.title)
super(Post, self).save(*args, **kwargs)
创建新文件并重命名 signals.py
(靠近 models.py
):import io
import sys
from PIL import Image
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.dispatch import receiver
from django.db.models.signals import pre_save, pre_delete
from .models import Post
# DRY
def image_resized(image, h):
name = image.name
_image = Image.open(image)
content_type = Image.MIME[_image.format]
r = h / _image.size[1] # ratio
w = int(_image.size[0] * r)
imageTemproaryResized = _image.resize((w, h))
file = io.BytesIO()
imageTemproaryResized.save(file, _image.format)
file.seek(0)
size = sys.getsizeof(file)
return file, name, content_type, size
@receiver(pre_save, sender=Post, dispatch_uid='post.save_image')
def save_image(sender, instance, **kwargs):
# add image (cover | thumbnail)
if instance._state.adding:
# postcover
file, name, content_type, size = image_resized(instance.postcover, 500)
instance.postcover = InMemoryUploadedFile(file, 'ImageField', name, content_type, size, None)
# postcover_tn
file, name, content_type, size = image_resized(instance.postcover_tn, 175)
instance.postcover_tn = InMemoryUploadedFile(file, 'ImageField', name, content_type, size, None)
# update image (cover | thumbnail)
if not instance._state.adding:
# we have 2 cases:
# - replace old with new
# - delete old (when 'clear' checkbox is checked)
# postcover
old = sender.objects.get(pk=instance.pk).postcover
new = instance.postcover
if (old and not new) or (old and new and old.url != new.url):
old.delete(save=False)
# postcover_tn
old = sender.objects.get(pk=instance.pk).postcover_tn
new = instance.postcover_tn
if (old and not new) or (old and new and old.url != new.url):
old.delete(save=False)
@receiver(pre_delete, sender=Post, dispatch_uid='post.delete_image')
def delete_image(sender, instance, **kwargs):
s = sender.objects.get(pk=instance.pk)
if (not s.postcover or s.postcover is not None) and (not s.postcover_tn or s.postcover_tn is not None):
s.postcover.delete(False)
s.postcover_tn.delete(False)
在 apps.py
:我们需要在
apps.py
中注册信号因为我们使用装饰器 @receiver
:from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class BlogConfig(AppConfig): # change to the name of your app
name = 'blog' # and here
verbose_name = _('Blog Entries')
def ready(self):
from . import signals
这是post
的第一个截图管理区由于缩略图是从帖子封面生成的,作为 UI/UX 良好实践,无需显示帖子封面缩略图的第二个输入文件(我将第二个图像字段保留在
admin.py
中只读)。下面是我上传图片后的第二个截图
PS:截图是从我正在开发的另一个应用程序中截取的,所以几乎没有变化,在你的情况下你应该看到
Currently: img/posts/8b0be417db564c53ad06cb493029e2ca.jpg
(参见 upload_cover()
中的 models.py
)代替 Currently: img/blog/posts/featured/8b0be417db564c53ad06cb493029e2ca.jpg
在
admin.py
# "img/posts/default.jpg" and "img/posts/default_thumbnail.jpg" are placeholders
# grab to 2 image placeholders from internet and put them under "/static" folder
def get_post_cover(obj):
src = obj.postcover.url if obj.postcover and \
hasattr(obj.postcover, 'url') else os.path.join(
settings.STATIC_URL, 'img/posts/default.jpg')
return mark_safe('<img src="{}" height="500" style="border:1px solid #ccc">'.format(src))
get_post_cover.short_description = ''
get_post_cover.allow_tags = True
def get_post_cover_thumbnail(obj):
src = obj.postcover_tn.url if obj.postcover_tn and \
hasattr(obj.postcover_tn, 'url') else os.path.join(
settings.STATIC_URL, 'img/posts/default_thumbnail.jpg')
return mark_safe('<img src="{}" height="175" style="border:1px solid #ccc">'.format(src))
get_post_cover_thumbnail.short_description = ''
get_post_cover_thumbnail.allow_tags = True
class PostAdmin(admin.ModelAdmin):
list_display = ('title', .. )
fields = (
'author', 'title', 'content',
get_post_cover, get_post_cover_thumbnail, 'postcover',
)
readonly_fields = (get_post_cover, get_post_cover_thumbnail)
[..]
最后你不需要任何 删除 save()
中的逻辑函数在 forms.py
关于python - Django - 保存图像的多个版本,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62739178/