Contents:

Django图片系统


1301 浏览 5 years, 3 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覆盖

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);
            }
        },
后台处理
URL

两个URL,分别处理文件上传和状态更新功能

urlpatterns = [
    url(r'^uploadfile', upload_file, name='upload_file'),
    url(r'^upload_status$', upload_status, name='upload_status'),
]

对应处理函数如下upload_statusupload_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)

至此,进度条上传和头像更新功能已完成,效果图如下