[英]Django - Save multiple versions of an Image
我的應用程序需要保存上傳圖像的多個版本。 一張高質量圖像和另一張僅用於縮略圖(低質量)。 目前這大部分時間都在工作,但有時保存方法會失敗,我的所有縮略圖都被刪除,尤其是當我在表單中使用 remove_cover 復選框時
raise ValueError("'%s' 屬性沒有與之關聯的文件。" % self.field.name) app | ValueError:“postcover_tn”屬性沒有與之關聯的文件。
-> 在此處查看完整跟蹤: 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)
看來我也無法正確解決:
forms.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
(參見models.py
中的upload_cover()
) 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)
[..]
最后,您在 forms.py 中的save()
forms.py
中不需要任何刪除邏輯
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.