Django Admin 上传多张图片并显示缩略图
文章目录
1.效果预览
需要的python库:因为要处理图片,必须安装pillow库。
2.自定义Widget
django Admin使用的图片上传Widget是:
<input type='file'>
非常丑陋,直接使用肯定是不行的,再说我们还要展示上传之后的图片的缩略图,因此必须自定义控件。
在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>
注意:里面有张背景图片,保存目录在
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)#注册模型和模型管理器,别忘了,否则不显示
这样就将上述定义的模型表单嵌入到admin在change/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 效果展示
做完上述配置,就可以上传图片并展示缩略图了。
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请求,提高响应速度,服务器上暂存的图片可以在保存到数据库之后全部清空,效果:
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_list的label参数要保持空值,否则会渲染出一个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