Django Admin 上传多张图片并显示缩略图

Django Admin 上传多张图片并显示缩略图

1.效果预览

定制效果预览

需要的python库:因为要处理图片,必须安装pillow库。

2.自定义Widget

django Admin使用的图片上传Widget是:

<input type='file'>

django admin使用的图片上传widget

非常丑陋,直接使用肯定是不行的,再说我们还要展示上传之后的图片的缩略图,因此必须自定义控件。

models.py中,自定义控件的python代码如下:

class ImageInput(ClearableFileInput):
    template_name = "upload_multi_img/image_multi_upload.html"

    def render(self, name, value, attrs=None, renderer=None):
        context = self.get_context(name, value, attrs)
        template = loader.get_template(self.template_name).render(context)
        return mark_safe(template)

注意:这里必须继承django原来的图片上传Widget——ClearableFileInput,否则包含这个Widget 嵌入的模板表单将会出现编码错误,也就是缺少图中的enctype属性,导致文件上传失败。

表单编码错误

Widget的HTML模板代码

就是上述代码中引入的image_multi_upload.html

{% load static %}
<div class="file-button" id="upload_image"
     style="background-image:  url('{% static 'upload_multi_img/add.png' %}');background-repeat: no-repeat;background-size: 64px;background-position: center">
    <input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %}
           value="{{ widget.value|stringformat:'s' }}"{% endif %} οnchange="uploadImage(this)"
           id="id_imageUpload" accept="image/jpeg,image/jpg,image/png,image/gif" multiple>
</div>
<style type="text/css">
    /*缩略图片样式*/
    .selected-img {
        width: 160px;
        height: 100px;
        position: relative;
        display: inline-block;
        overflow: hidden;
        border: solid #b1c6c1 1px;
        border-radius: 10px;
        margin-right: 10px;
    }
  /*缩略图片聚焦样式*/
    .selected-img:hover {
        border: solid #25adc6 2px;
    }
/*上传图片按钮样式*/
    .file-button {
        width: 160px;
        height: 100px;
        position: relative;
        display: inline-block;
        overflow: hidden;
        border: solid #b1c6c1 1px;
        border-radius: 10px;
        margin-right: 10px;
    }
/*上传图片按钮聚焦样式*/
    .file-button:hover {
        border: solid #25adc6 2px;
    }

    .file-button input {
        position: absolute;
        top: 0;
        height: 100px;
        opacity: 0;
    }
/*删除缩略图片icon聚焦样式*/
    i:hover {
        color: #0081C6;
    }
/*阿里云字体图标*/
    @font-face {
        font-family: 'iconfont';  /* project id 1361777 */
        src: url('//at.alicdn.com/t/font_1361777_ufufbwqmfpa.eot');
        src: url('//at.alicdn.com/t/font_1361777_ufufbwqmfpa.eot?#iefix') format('embedded-opentype'),
        url('//at.alicdn.com/t/font_1361777_ufufbwqmfpa.woff2') format('woff2'),
        url('//at.alicdn.com/t/font_1361777_ufufbwqmfpa.woff') format('woff'),
        url('//at.alicdn.com/t/font_1361777_ufufbwqmfpa.ttf') format('truetype'),
        url('//at.alicdn.com/t/font_1361777_ufufbwqmfpa.svg#iconfont') format('svg');
    }

    .iconfont {
        font-family: "iconfont" !important;
        font-size: 20px;
        font-style: normal;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }

    .icon-delete:before {
        content: "\e63c";
    }
</style>

注意:里面有张背景图片,保存目录在

add背景图片目录

3.定义模型和模型表单

3.1 定义模型

models.py中定义一个数据模型

class UploadModel(models.Model):
     images = models.FileField('图片', upload_to="static/kaopu_shop/UploadModel")
     ......

3.2 定义模型表单

models.py中定义上述模型的模型表单

class UploadForm(ModelForm):
    images = forms.FileField(label="图片", widget=ImageInput, help_text="按住ctrl多选,最多4张", required=False)

    class Meta:
        model = UploadModel
        fields = ['images']

注意:在ModelForm中重写了模型中的images字段,并且必须是ModelFrom类型,否则无法嵌入模型管理器。

3.3 定义模型管理器

admin.py中定义对应的模型管理器

class UploadModelAdmin(admin.ModelAdmin):
    form = UploadForm
    
admin.site.register(UploadModel, UploadModelAdmin)#注册模型和模型管理器,别忘了,否则不显示

这样就将上述定义的模型表单嵌入到adminchange/add界面展示的表单里。如图:

自定义控件渲染效果

3.4 数据库迁移

做完上述操作之后需要进行数据库迁移,将模型写入数据库

>python manage.py makemigrations
>python manage.py migrate

4.Admin使用ajax上传图片并显示缩略图

因为这里只需要上传图片并显示图片缩略图,不是保存整个admin表单,因此必须使用Ajax来实现。这里需要引入jQuery

4.1 设置settings.py

settings.py末尾中添加以下内容

# 媒体文件根路径和URL
MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\\', '/')  # media即为图片上传的根路径
MEDIA_URL = '/media/'

# 定义主机IP
HOST = "http://127.0.0.1:8000/"
# 定义图片暂存目录和模型图片目录
TEMP_IMAGE_SUB_DIR = "/tempimg/"
# 图片暂存目录
TEMP_IMAGE_DIR = MEDIA_ROOT + TEMP_IMAGE_SUB_DIR
# 模型图片目录
MODEL_IMAGE_SUB_DIR = "/model_images/"
MODEL_IMAGE_DIR = MEDIA_ROOT + MODEL_IMAGE_SUB_DIR
# 定义缩略图URL:http://127.0.0.1:8000/mdedia/tempimg/
WEB_HOST_MEDIA_URL = HOST+ MEDIA_URL[1:]+ TEMP_IMAGE_SUB_DIR
# 定义模型中保存的图片URL:http://127.0.0.1:8000/mdedia/model_images/
MODEL_MEDIA_URL = HOST+ MEDIA_URL[1:]+ MODEL_IMAGE_SUB_DIR

4.2 模板脚本

在模板中添加ajax脚本

{#引入jQuery的CDN#}
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript">
    //必须加这个请求头,否则ajax请求被django屏蔽,这行代码不能放进function中声明,否则获取不到请求头
    var csrftoken = $.cookie('csrftoken');
    function uploadImage(obj) {
        var formData = new FormData();
        var files = $("#id_imageUpload")[0].files;//获取模板定义的图片上传按钮的文件
        //如果图片数目大于4,弹出警告
        if (files.length > 4) {
            alert("最多选择4张图片!");
            return;
        }
        //检查图片数目和图片类型,只允许上传jpg,png,gif格式
        if (0 < files.length && files.length < 5) {
            for (i = 0; i < files.length; i++) {
                var ext = files[i].name.slice(files[i].name.lastIndexOf(".") + 1).toLowerCase();
                if ("png" == ext || "jpg" == ext || "jpeg" == ext || "gif" == ext) {
                    formData.append(files[i].name, files[i]);
                }
            }
        }

        if (formData) {
            //必须加上csrftoken ,否则验证不通过,ajax请求无效
            $.ajax({
                url: '{% url 'upload_multi_img:upload_temp_images' %}',
                dataType: 'json',// 返回值类型 一般设置为json
                type: 'POST',
                headers: {"X-CSRFToken": csrftoken},//django默认拒绝post请求,必须加 csrftoken,否则请求被屏蔽
                processData: false,    // 告诉jQuery不要去处理发送的数据
                contentType: false, //告诉jQuery不检查类型
                data: formData,
                async: false,//必须设置为同步模式,否则success方法没有返回值
                success: function (data) {

                    //console.log(data["image_list"])
                    //动态添加HTML元素,显示上传的图片
                    for (i = 0; i < data["image_list"].length; i++) {
                        //console.log(data["image_list"][i])
                        $("#upload_image").before("<div class=\"selected-img\">\n" +
                            "    <i class=\"iconfont icon-delete\" style=\"z-index: 999;background-color:rgba(255,255,255,.8);position: absolute;right: 3px;top: 3px;\" title=\"删除图片\" οnclick=\"delete_img(this)\"></i>\n" +
                            "    <img src=\"" + data["image_list"][i] + "\" alt=\"待选图片\" style=\"width: 160px;height: 100px;border-radius: 10px;\">\n" +
                            "</div>");
                    }
                    alert("上传成功", data["msg"]);
                },
                error: function (error) {
                    alert("服务器异常");
                }
            })
        }
        return false;
    }
</script>

注意:

  • 必须设置csrftoken,否则请求被django视为不安全而被屏蔽。
  • ajax必须设置为同步模式,否则没有返回值,获取不到后端返回的缩略图URL。

4.2 视图函数

首先在settings.py中添加一下:

WEB_HOST_MEDIA_URL = os.path.join('http://127.0.0.1:8000/', MEDIA_URL[1:], 'tempimg/')

然后编写视图函数

import json
import os
import uuid

from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render

# Create your views here.
from django.views.decorators.csrf import csrf_exempt

from upload_image.settings import WEB_HOST_MEDIA_URL
@login_required
@csrf_exempt
def upload_temp_image(request):
    result = {}
    if request.method == 'POST':
        files = request.FILES
        if files:
            image_url_list = []
            for file_name in files:
                image_url_list.append(handle_uploaded_file(files.get(file_name)))  # 处理上传文件
            result = {'msg': 'success', "image_list": image_url_list, }

        else:
            result = {'msg': 'failed', "image_list": []}
    return HttpResponse(json.dumps(result, ensure_ascii=False), content_type="application/json,charset=utf-8")  # 返回json

# 处理上传的文件
def handle_uploaded_file(file):
    # 分割文件名,提取拓展名
    extension = os.path.splitext(file.name)
    # 使用uuid4重命名文件,防止重名文件相互覆盖
    #注意首先在项目的根目录下新建media/tempimg,或者自己使用python代码创建目录
    file_name = '{}{}'.format(uuid.uuid4(), extension[1])
    with open(TEMP_IMAGE_DIR + file_name, 'wb+') as destination:
        for chunk in file.chunks():#防止文件太大导致内存溢出
            destination.write(chunk)
    # 返回图片的URL
    return os.path.join(WEB_HOST_MEDIA_URL, file_name)

4.3 url设置

在应用的urls.py中声明url

from django.urls import path
from upload_multi_img import views
app_name = 'upload_multi_img'
urlpatterns = [
    path('upload_temp_image/', views.upload_temp_image, name="upload_temp_images")
]

在项目的urls.py中引入应用的url

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',include('upload_multi_img.urls'),name="upload_multi_img")
]

4.4 显示缩略图

完成上面的配置之后,已经能上传图片到django后端了,但是还不能显示缩略图,因为没有配置缩略图显示URL,还需进行以下配置。

在项目的urls.py中配置缩略图显示路径:

from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include

from upload_image import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',include('upload_multi_img.urls'),name="upload_multi_img")
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)  # 通过URL访问图片

4.5 效果展示

做完上述配置,就可以上传图片并展示缩略图了。

缩略图展示效果.gif

5. 删除缩略图

注意到目前为止,上传的图片还没有保存到我们的数据库,只是暂存在服务器上,有的时候用户上传图片之后可能需要临时更改,这时候就需要删除缩略图。在之前的模板中添加以下脚本:

//删除选中的图片,不能使用click,因为动态添加的不能绑定click
    function delete_img(e) {
        $(e).closest('.selected-img').remove();
    }

实现点击删除图标删除使用jquery动态添加的缩略图组件:

<div class="selected-img">
    <i title="删除图片" class="iconfont icon-delete" style="z-index: 999;background-color:rgba(255,255,255,.8);position: absolute;right: 3px;top: 3px;" onclick="delete_img(this)"></i>
    <img style="width: 160px;height: 100px;border-radius: 10px;" alt="待选图片" src=data["image_list"][i]>
</div>

注意这里只是删除了浏览器界面渲染的HTML元素,并没有删除服务器上暂存的图片,这样做的目的是减少ajax请求,提高响应速度,服务器上暂存的图片可以在保存到数据库之后全部清空,效果:

删除缩略图.gif

6. 缩略图预览

为了方便用户使用,缩略图还需要有查看大图的功能,在模板中添加以下脚本:

{#显示大图#}
<div id="outerdiv"
     style="position:fixed;top:0;left:0;background:rgba(0,0,0,0.7);z-index:10000;width:100%;height:100%;display:none;">
    <div id="innerdiv" style="position:absolute;">
        <img id="bigimg" style="border:5px solid #fff;" src="" alt="大图"/>
    </div>
</div>
#显示大图#}
<script type="text/javascript">
    //因为图片是动态添加的,所以不能使用选择器选择。
    function show_big_img(obj) {
        imgShow("#outerdiv", "#innerdiv", "#bigimg", obj);
    }

    function imgShow(outerdiv, innerdiv, bigimg, obj) {
        var src = obj.src;//获取当前点击的pimg元素中的src属性
        $(bigimg).attr("src", src);//设置#bigimg元素的src属性
        console.log("¥¥¥¥", obj.height, obj.width);
        var windowW = $(window).width();//获取当前窗口宽度
        var windowH = $(window).height();//获取当前窗口高度
        var realWidth = obj.naturalWidth;//获取图片真实宽度
        var realHeight = obj.naturalHeight;//获取图片真实高度
        var imgWidth, imgHeight;
        var scale = 0.8;//缩放尺寸,当图片真实宽度和高度大于窗口宽度和高度时进行缩放

        if (realHeight > windowH * scale) {//判断图片高度
            imgHeight = windowH * scale;//如大于窗口高度,图片高度进行缩放
            imgWidth = imgHeight / realHeight * realWidth;//等比例缩放宽度
            if (imgWidth > windowW * scale) {//如宽度扔大于窗口宽度
                imgWidth = windowW * scale;//再对宽度进行缩放
            }
        } else if (realWidth > windowW * scale) {//如图片高度合适,判断图片宽度
            imgWidth = windowW * scale;//如大于窗口宽度,图片宽度进行缩放
            imgHeight = imgWidth / realWidth * realHeight;//等比例缩放高度
        } else {//如果图片真实高度和宽度都符合要求,高宽不变
            imgWidth = realWidth;
            imgHeight = realHeight;
        }
        $(bigimg).css("width", imgWidth);//以最终的宽度对图片缩放
        var w = (windowW - imgWidth) / 2;//计算图片与窗口左边距
        var h = (windowH - imgHeight) / 2;//计算图片与窗口上边距
        $(innerdiv).css({"top": h, "left": w});//设置#innerdiv的top和left属性
        $(outerdiv).fadeIn("fast");//淡入显示#outerdiv及.pimg

        $(outerdiv).click(function () {//再次点击淡出消失弹出层
            $(this).fadeOut("fast");
        });
    }


</script>

效果如下:

缩略图预览

7.保存图片到数据库

完成上述的设置之后,还需要在用户点击django admin表单的保存按钮之后将用户上传的图片保存到数据库。因为django的模型字段ImageField不支持保存多张图片,因此直接使用TextField字段保存所有上传图片的URL

7.1 自定义一个Widget

这里自定义一个Widget是为了将图片URL上传和Admin界面的表单上传合并在一起,减少Ajax请求,也是为了能使用模型表单同时处理Admin表单数据和图片列表数据,这个Widget是隐藏在页面中不显示的。

upload_img_list.html

<Textarea  name="images_list" id="images_list" style="height: 200px;width: 300px;" ></Textarea>

注意:必须加上name属性,否则数据不会在表单提交的时候被提交。

python代码

class UploadImageList(TextInput):
    template_name = "upload_multi_img/upload_img_list.html"

    def render(self, name, value, attrs=None, renderer=None):
        context = self.get_context(name, value, attrs)
        template = loader.get_template(self.template_name).render(context)
        return mark_safe(template)

7.2 修改模板脚本

修改之前模板脚本中的 function delete_img和ajax的success回调函数。

<script type="text/javascript">

    var image_list = [];
    var csrftoken = $.cookie('csrftoken');
	
    //删除选中的图片,不能使用click,因为动态添加的不能绑定click
    function delete_img(e) {
        image_list.splice($.inArray($(e).closest('.selected-img').children("img").attr("src"), image_list), 1);//移除缩略图
        $("#images_list").val(image_list.join(','));
        $(e).closest('.selected-img').remove();
    }

    function uploadImage(obj) {
       .......

        if (formData) {
            //必须加上csrftoken ,否则验证不通过,ajax请求无效
            $.ajax({
               ....
                success: function (data) {

                    .....
                        //保存返回的图片URL到列表中
                        var index = $.inArray(data["image_list"][i], image_list);
                        if (index < 0) {
                            image_list.push(data["image_list"][i])
                        }
                    }
                    var list = image_list.join(',');  //list是以,分割的字符串
                    $("#images_list").val(list);
                    alert("上传成功", data["msg"]);
                },
                error: function (error) {
                  ...
                }
            })
        }
        return false;
    }
</script>

7.3 修改模型和模型表单

class UploadModel(models.Model):
    images = models.FileField('图片', upload_to="static/upload_multi_img/")
    images_list = models.CharField('', max_length=10000)

    def save(self, *args, **kwargs):
        # 阻止images字段的数据保存在数据库中,因为我们不需要
        self.images = ""
        model_images = []
        print(self.images_list)
        # 将暂存目录中的图片转存到正式目录
        for root, dirs, files in os.walk(TEMP_IMAGE_DIR):
            print('files:', files)
            for file in files:
                if os.path.join(WEB_HOST_MEDIA_URL, file) in self.images_list:
                    shutil.move(TEMP_IMAGE_DIR + file, MODEL_IMAGE_DIR + file)
                    model_images.append(os.path.join(MODEL_MEDIA_URL, file))

        # 清空暂存目录下所有图片
        shutil.rmtree(TEMP_IMAGE_DIR)
        os.mkdir(TEMP_IMAGE_DIR)
        # 将模型原来的图片URL换为存到正式目录后的URL
        self.images_list = model_images
        # 必须调用父类的方法,否则数据不会保存
        super().save(*args, **kwargs)


class UploadForm(ModelForm):
     images = forms.FileField(label="图片", widget=ImageInput, help_text="按住ctrl多选,最多4张", required=False)
    images_list = forms.CharField(label='', widget=UploadImageList, help_text='', required=False)

    class Meta:
        model = UploadModel
        fields = ['images', 'images_list']

注意: images_listlabel参数要保持空值,否则会渲染出一个label标签。

经过上面的修改,就能把图片保存到数据库中了,效果:

8.显示模型中保存的图片

上面已经把图片URL以字符串的形式保存在数据库中了,接下来还需要把保存的图片显示在admin界面上。

修改upload_img_list.html为:

<Textarea name="images_list" id="images_list"
          style="height: 200px;width: 300px;" hidden>{{ widget.value|stringformat:'s' }}</Textarea>

image_multi_upload.html再添加一个脚本

{#显示模型中保存的图片#}
<script type="text/javascript">
    $(document).ready(function () {
        var model_image_list = $("#images_list").val().split(",")
        //如果$("#images_list").val()为空,会返回一个"None"字符串
        if (model_image_list.length > 1) {
            for (i = 0; i < model_image_list.length; i++) {
                $("#upload_image").before("<div class=\"selected-img\">\n" +
                    "    <i class=\"iconfont icon-delete\" style=\"z-index: 999;background-color:rgba(255,255,255,.8);position: absolute;right: 3px;top: 3px;\" title=\"删除图片\" οnclick=\"delete_img(this)\"></i>\n" +
                    "    <img src=\"" + model_image_list[i].replace("'", '').replace("'", '') + "\" alt=\"待选图片\" style=\"width: 160px;height: 100px;border-radius: 10px;\" οnclick=\"show_big_img(this)\">\n" +
                    "</div>");
            }
        }
    })
</script>

这样就能显示模型中保存的图片了。效果:
模型图片显示

9. 在数据被删除的时候删除图片

最后,还需要在数据被删除时删除保存的图片,防止产生大量的垃圾图片。在models.py中添加一个信号接收器,接收模型被删除的信号:

# 删除被删除的模型的图片
@receiver(post_delete, sender=UploadModel)
def delete_upload_files(sender, instance, **kwargs):
    image_list = getattr(instance, 'images_list', '')

    if not image_list:
        return
    else:
        # 去除image_list中URL存在的''字符
        list = image_list.replace("'", "").replace("'", "").split(",")
        # 删除被删除的模型的图片
        for image in list:
            # 获取文件名
            delete_image_name = image.split('/')[-1]
            os.remove(MODEL_IMAGE_DIR + delete_image_name)


class UploadForm(ModelForm):
    .......

注意:这里使用信号接收器而不是重写模型delete方法是因为批量删除模型的时候不会调用模型的delete方法。

10.开发建议

django是一个非常强大的框架,但是唯一不足的地方是它的Admin界面非常丑陋,本文通过一系列定制在Admin实现了多张图片上传,显示缩略图,缩略图预览,保存多张图片到数据库的功能。按照这样的方式还可以做一定的拓展,比如上传文件时显示相应的文件图标等。

完整源码链接:链接: https://pan.baidu.com/s/1Sa8cEg19bNqoDkiCGEWaog 提取码: fb49
注意:python版本是python3.6.8,django版本是2.2.6,开发工具是pycharm2019

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安布奇

喜欢的朋友给点支持和鼓励吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值