Django图片系统
1477 浏览 5 years, 10 months
3 上传进度条
版权声明: 转载请注明出处 http://www.codingsoho.com/上传进度条
本文参考:https://github.com/Tonetete/Simple-Django-progressbar-upload-file-form-with-ajax
另外,Upload to Django with progress bar using Ajax and jQuery有相对详细的讲解,但是内容不全
它的处理过程如下:
- 客户端在完全传完文件之前一直用AJAX请问获取最新状态
- 后端处理这个请求,并返回JSON数据,内容包含当前传输了多少数据
- 数据传输的内容在文件上传句柄里完成,通过它的
receive_data_chunk
函数更新,并存储在cache里供后端视图函数读取。 - 如果有多人传输请求,怎么区分各个请求呢?本例的方案是每个请求对应一个
csrftoken
,在初始化handle_raw_input
时,将这个token作为数据缓存的key;每个请求都需要带一个csrfmiddlewaretoken
,根据不同的token返回不同的值。
所有的上传相关的内容都封装成了一个库 fileuploadwrapper
fileuploadwrapper
UploadProgressCachedHandler
文件上传参考函数 https://docs.djangoproject.com/en/1.8/topics/http/file-uploads/
下面是两个主要处理函数,handle_raw_input
时,从cookie读到token,并初始化cache。后接收到数据块函数receive_data_chunk
里,根据当前状态,不断的更新这个cache。
class UploadProgressCachedHandler(FileUploadHandler):
def handle_raw_input(self, input_data, META, content_length, boundary, encoding=None):
self.content_length = content_length
if 'CSRF_COOKIE' in self.request.GET:
self.cache_key = self.request.GET['CSRF_COOKIE']
elif 'CSRF_COOKIE' in self.request.META:
self.cache_key = self.request.META['CSRF_COOKIE']
if self.cache_key:
cache.set(self.cache_key, {
'totalsize': self.content_length,
'uploaded': 0
})
else:
pass
def receive_data_chunk(self, raw_data, start):
if self.cache_key:
data = cache.get(self.cache_key)
data['uploaded'] += self.chunk_size
cache.set(self.cache_key, data)
return raw_data
AJAX 请求
文件upload/ajax.html
前端ajax请求有两个,upload file 和 upload status,一个用于请求上传文件,一个用去获取上传状态。
上传文件
var options = {
beforeSubmit: showRequest, // pre-submit callback
success: showResponse, // post-submit callback
error: showError,
// other available options:
//url: "/personalcenter/uploadfile", // override for form's 'action' attribute
url: "{% url 'upload_file' %}", // override for form's 'action' attribute
type: "post", // 'get' or 'post', override for form's 'method' attribute
dataType: "json" // 'xml', 'script', or 'json' (expected server response type)
//clearForm: true // clear all form fields after successful submit
//resetForm: true // reset the form after successful submit
// $.ajax options can be used here too, for example:
//timeout: 3000
};
// bind to the form's submit event
$('#form_upload').submit(function() {
// inside event callbacks 'this' is the DOM element so we first
// wrap it in a jQuery object and then invoke ajaxSubmit
// disable submit button for prevent requests
$("#buttonSubmit").prop("disabled", true);
$(".processing-file").html("");
$(this).ajaxSubmit(options);
$(".progress").show();
//sleep(150); // hebin workaround to avoid call upload_status immediately, if it's the 2nd upload
//progressWorker(url);
ShowProgress();
// !!! Important !!!
// always return false to prevent standard browser submit and page navigation
return false;
});
提交请求后,立即调用ShowProgress()函数来更新状态。
更新状态
该函数里,还是一个ajax请求,收到成功回复后,更新进度条。如果没到100%完成,那么在complete函数继续嵌套调用自己。
function ShowProgress(){
sleep(150);
url = "{% url 'upload_status' %}?key=" + $("form#form_upload input[name=csrfmiddlewaretoken]").val(); progressWorker(url);
}
function progressWorker(url){
percent = 0;
$.ajax({
url: url,
async: true,
dataType: "json",
contentType: "application/json",
success: function (progress) {
if(progress.uploaded && progress.totalsize){
percent = (progress.uploaded/progress.totalsize) * 100;
percent = parseInt(percent, 10);
$('.progress-bar').css('width', percent+'%').attr('aria-valuenow', percent);
$('.progress-bar').html(percent+"%");
/* Call sleep function before make a new request in order to prevent much
request to server. Use it wisely... */
//sleep(1000);
}
},
complete: function(){
if(percent<100){
progressWorker(url);
}
else{
$(".processing-file").show();
$(".processing-file").append('<span class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span> Processing file, please wait...');
}
},
error: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.status == 500) {
alert('Internal error: ' + jqXHR.responseText);
} else {
alert('Unexpected error. status : ' + jqXHR.status);
}
}
});
}
这两个ajax事件,处理上用了不同的实现方法
/fileupload/upload_status使用了常用的$.ajax
/fileupload/uploadfile使用ajaxSubmit,form里指定的action会被options覆盖
- ajaxsubmit在jquery.form中定义,需要引用该js文件
jquery中各个事件执行顺序如下:
1. ajaxStart(全局事件)
2. beforeSend
3. ajaxSend(全局事件)
4. success当请求成功时调用函数,即status==200;
5. ajaxSuccess(全局事件)
6. error
7. ajaxError (全局事件)
8. complete当请求完成时调用函数,即status==404、403、302...只要不出错就行
9. ajaxComplete(全局事件)
10.ajaxStop(全局事件)
进度条更新
ajax获取状态的反馈之后,会持续更新进度条状态。
修改的内容包括 css (width), attr (aria-valuenow),以及html显示值。具体看下面代码即可。
function progressWorker(url){
percent = 0;
$.ajax({
url: url,
async: true,
dataType: "json",
contentType: "application/json",
success: function (progress) {
// console.log("progressWorker:success");
if(progress.uploaded && progress.totalsize){
console.log("Percent: "+ percent + "%");
percent = (progress.uploaded/progress.totalsize) * 100;
percent = parseInt(percent, 10);
$('.progress-bar').css('width', percent+'%').attr('aria-valuenow', percent);
$('.progress-bar').html(percent+"%");
/* Call sleep function before make a new request in order to prevent much
request to server. Use it wisely... */
//sleep(1000);
}else{
// console.log(progressWorker: else - progress);
}
},
- progressbar 相关引用文件及参考
后台处理
URL
两个URL,分别处理文件上传和状态更新功能
urlpatterns = [
url(r'^uploadfile', upload_file, name='upload_file'),
url(r'^upload_status$', upload_status, name='upload_status'),
]
对应处理函数如下upload_status
和upload_file
更新状态
progressWorker发出的对应请求格式如下
[01/Jul/2018 16:29:52] "GET /fileupload/upload_status?key=tq3kigprjepAOOk4gNjwdXyiryovLK9Kt9cFw03qLJjG5rOIkbATJMBVGZOQA0Ie HTTP/1.1" 200 39
处理函数里,首先从URL GET参数里读到这个key,即csrfmiddlewaretoken,然后查找cache这个对应的数据是否存在,如果有,那么读取数据并返回。
ajax的传值函数如下
url = "{% url 'upload_status' %}?key=" + $("form#form_upload input[name=csrfmiddlewaretoken]").val();
特别说明的是,跟djagno 1.9相比,django 1.11在csrf的处理上做了变化,csrfmiddlewaretoken和csrftoken的值不字面相等,中间经过转换,需要通过比较函数_compare_salted_tokens
来确认。具体可参考我的csrf文章。
def upload_status(request):
if request.method == 'GET':
if request.GET['key']:
csrftoken = request.META.get("CSRF_COOKIE") if "CSRF_COOKIE" in request.META else None
key = request.GET['key']
request_csrftoken = _sanitize_token(key)
cache_exist = cache.get(csrftoken)
match = _compare_salted_tokens(request_csrftoken, csrftoken)
if cache_exist and match:
value = cache.get(csrftoken)
return HttpResponse(json.dumps(value), content_type="application/json")
elif not _compare_salted_tokens(key, csrftoken):
return HttpResponse(json.dumps({'error':"csrf value not match"}), content_type="application/json")
else:
return HttpResponse(json.dumps({'error':"No csrf value in cache"}), content_type="application/json")
else:
return HttpResponse(json.dumps({'error':'No parameter key in GET request'}), content_type="application/json")
else:
return HttpResponse(json.dumps({'error':'No GET request'}), content_type="application/json")
上传文件完成
请求如下
[01/Jul/2018 16:29:54] "POST /fileupload/uploadfile HTTP/1.1" 200 64
处理函数没什么特别的,跟正常的文件上传差不多。当完成传输完成后,该函数会被调用。
因为上传在库文件处理的,所以这儿的视图上传成功后并不会进行逻辑处理。一种办法是将上传好的文件存到cache里,让应用在合适的地方处理。
def upload_file(request):
if request.method == 'POST':
upload_form = UploadFileForm(request.POST, request.FILES)
if upload_form.is_valid():
in_mem_image_file=request.FILES['image']
filepath = os.path.join(get_upload_path(), in_mem_image_file.name)
if in_mem_image_file:
img = Image.open(in_mem_image_file)
img.save(filepath)
cache.set('cache_key_upload', os.path.join(get_upload_url(), in_mem_image_file.name) ,60*15)
return HttpResponse(json.dumps({'message': 'Upload complete!','url': os.path.join(get_upload_url(), in_mem_image_file.name)}))
else:
return HttpResponse(json.dumps({'message': 'invalid form!'}))
else:
form = UploadFileForm()
return HttpResponse(json.dumps({'message': 'invalid form!'}))
项目访问位置:
http://demo.codingsoho.com/authwrapper/user/1/edit/avatar 从用户中心访问
更新用户头像
接下来使用该库替换前面的FormView前台上传方法, 实现动态更新头像。
添加库
添加配置,引入fileuploadwrapper
from django.conf import global_settings
FILE_UPLOAD_HANDLERS = ['fileuploadwrapper.uploadfilehandler.UploadProgressCachedHandler', ] \
+ global_settings.FILE_UPLOAD_HANDLERS
前端上传
首先在authwrapper.forms.py里面新增加一个图片上传的form
class UploadFileForm(forms.Form):
image = forms.ImageField(widget=forms.FileInput(
attrs={'required': 'required'})) # required=True is the default, but not show it validation in template
设置required,这样如果不选图片的话,上传会报错
对应的template如下 userprofile_detail.html
注意 : 有几个必须保持一致的class和id, 这些在fileuploadwrapper里固定写死了。
fom.id = form_upload
input.id = buttonSubmit
<form id="form_upload" enctype="multipart/form-data" action="{% url 'upload_file' %}" method="POST"> {% csrf_token %}
<img id="img" class="img-responsive" src="{% if object.image %}{{object.image.url}}{% endif %}">
{{upload_form}}
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
0%
</div>
</div>
<div class="processing-file"></div>
<input id="buttonSubmit" class="btn btn-primary" type="submit" value="Upload" />
</form>
这热Upload按钮会用于上传图片,上面代码是进度条的显示内容
添加新的js和css来支持ajaxsubmit和progressbar
<script src="{% static 'js/jquery.form.js' %}"></script>
<script src="{% static 'js/jquery-ui.js' %}"></script>
<link href="{% static 'css/jquery-ui.css' %}" rel="stylesheet">
可以用它们的CDN
<link rel="stylesheet" href="[https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.css](https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.css)">
<script src="[https://cdn.bootcss.com/jquery.form/4.2.2/jquery.form.js](https://cdn.bootcss.com/jquery.form/4.2.2/jquery.form.js)"></script>
<script src="[https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.js](https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.js)"></script>
记得将ajax.html 引入到当前模板
{% block head_js %}
{% include "upload/ajax.html" %}
{% endblock %}
最后剩下一个很重要的问题是,上面的处理都是在库里,逻辑的内容在哪儿处理呢?如何将已上传的图片更新到用户DetailView呢? 本文使用的一个方法是在后面再放一个form去提交,当然应该有更好的方法来避免用户二次操作。
PS:嵌套form好像不支持。
因为要替换前面的FormView上传图片的方法,所以建立重用这个form就可以了
文件userprofile_detail.html
<form action="" enctype="multipart/form-data" method="post">{% csrf_token %}
{# <input type="file" name="image" id="id_image"> #}
<input type="submit" value="Update" />
</form>
这样除了图片上传的表单,界面上还会出现提交的按钮,这个在另外一个独立的表单里。没有指定url, 由默认post函数处理。
视图处理
图片上传完成之后,需要将它附着到User对象。这儿通过upload_file里设置的cache: cache_key_upload
DetailView函数context里面传递了两个form, 一个是正常的FormMixin
带的上传图片的表单,另一个上传完成后做post更新的表单。
处理POST时,从cache中读取当前的image信息,并将它赋给User.image对象。
class UserProfileDetailUpdateImageView(FormMixin, DetailView):
model = UserModel
template_name = 'auth/userprofile_detail.html'
form_class = UserUpdateImageForm
#
def get_context_data(self, *args, **kwargs):
context = super(UserProfileDetailUpdateImageView, self).get_context_data(*args, **kwargs)
context["form"] = self.form_class(instance = self.get_object())
context["upload_form"] = UploadFileForm()
return context
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
usermodel = UserModel().objects.get(id=self.kwargs.get("pk"))
image = None
if cache.has_key('cache_key_upload') and cache.get('cache_key_upload',None):
image = cache.get('cache_key_upload')
cache.delete('cache_key_upload')
if image:
usermodel.image = image
usermodel.save()
return self.form_valid(form)
else:
return self.form_invalid(form)
至此,进度条上传和头像更新功能已完成,效果图如下