Django - 保存图像的多个版本

Django - Save multiple versions of an Image

我的应用程序需要保存上传图像的多个版本。一张高质量图像和另一张仅供缩略图使用的图像(低质量)。 目前这在大部分时间都有效,但有时保存方法会失败,我的所有缩略图图像都会被删除,尤其是当我在表单 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

models.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管理区的第一个屏幕截图

由于缩略图是从 post 封面生成的,作为 UI/UX 良好做法,不需要显示 post 封面缩略图的第二个输入文件(我保留了第二个图像字段只读 admin.py).

下面是我上传图片后的第二张截图

PS:屏幕截图是从我正在开发的另一个应用程序中截取的,所以变化不大,在你的情况下你应该看到

  • Post 封面 改为特色图片
  • 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() 函数中的任何 delete 逻辑