複雑なアップローダーのためにすべてのピースをまとめるのは複雑な作業なので、ここで私のソリューションを共有します。
この回答の要素
ページごとに任意の数のアップローダーを配置できます。この例では、ページに Driving と Medical という 2 つの個別のアップロード ボタンがあります。
アップロードする前に、クライアント側で画像のサイズを 1024 x 1024 に変更します。
アップロード中にユーザーにアップロードされた画像のプレビューを表示する
Jinja 2 テンプレートとしての HTML コード
Bootstrap 3.x をテーマにしたアップロード ボタンとアップロードの進行状況バー
ピラミッド スタイルの JavaScript リソースの読み込み
1 つの個別のアップロード ウィジェットをレンダリングする Jinja 2 HTML テンプレート コード ( upload_snippet.html
)。パラメータidとnameおよびupload_targetを使用します。
<div id="upload-{{ id }}">
<div id="medical-license" class="btn btn-block btn-file">
<i class="fa fa-camera"></i> {{ name }}
<input type="file" class="file-select" data-url="{{ upload_target }}" data-param-name="{{ id }}">
</div>
<p>
<div class="preview" style="display: none"></div>
</p>
<div class="progress progress-xxx" style="display: none">
<div class="progress-bar progress-bar-striped progress-bar-xxx active" role="progressbar" aria-valuenow="45" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
</div>
</div>
<div class="success" style="display: none">
<div class="alert alert-success">
{{ name }} upload completed
</div>
</div>
<div class="error" style="display: none">
<div class="alert alert-danger">
<span class="error-message"></span>
</div>
</div>
</div>
2 つのアップロード ウィジェットを構築するメインの Jinja 2HTML テンプレート。Bootstrap ボタンのようにスタイルを設定します。
{% extends "site/base.html" %}
{% block extra_head %}
<style>
/* http://www.abeautifulsite.net/whipping-file-inputs-into-shape-with-bootstrap-3/ */
.btn-file {
position: relative;
overflow: hidden;
}
.btn-file input[type=file] {
position: absolute;
top: 0;
right: 0;
min-width: 100%;
min-height: 100%;
font-size: 100px;
text-align: right;
filter: alpha(opacity=0);
opacity: 0;
outline: none;
background: white;
cursor: inherit;
display: block;
}
.preview {
width: 128px;
height: 128px;
margin: 0 auto;
}
</style>
{% endblock %}
{% block content_section %}
<!-- Header -->
<section id="license-information">
<div class="row">
<div class="col-md-12">
<h1>Upload information</h1>
</div>
</div>
<div class="row">
<div class="col-md-6">
{% with id='medical', name='Medical license' %}
{% include "upload_snippet.html" %}
{% endwith %}
{% with id='driving', name='Driving license or other id' %}
{% include "upload_snippet.html" %}
{% endwith %}
</div>
</div>
</section>
{% endblock content_section %}
{% block custom_script %}
<!-- The jQuery UI widget factory, can be omitted if jQuery UI is already included -->
<script src="{{ 'xxx:static/jquery-file-upload/js/vendor/jquery.ui.widget.js'| static_url }}"></script>
<!-- The Load Image plugin is included for the preview images and image resizing functionality -->
<script src="{{ 'xxx:static/jquery-file-upload/js/load-image.all.min.js' | static_url }}"></script>
<!-- The Canvas to Blob plugin is included for image resizing functionality -->
<script src="{{ 'xxx:static/jquery-file-upload/js/canvas-to-blob.js' | static_url }}"></script>
<!-- The basic File Upload plugin -->
<script src="{{ 'xxx:static/jquery-file-upload/js/jquery.fileupload.js' | static_url }}"></script>
<!-- The File Upload processing plugin -->
<script src="{{ 'xxx:static/jquery-file-upload/js/jquery.fileupload-process.js' | static_url }} "></script>
<!-- The File Upload image preview & resize plugin -->
<script src="{{ 'xxx:static/jquery-file-upload/js/jquery.fileupload-image.js' | static_url }} "></script>
<script>
window.nextURL = "{{ after_both_files_are_uploaded }}"
</script>
<script>
"use strict";
var state = {
medical: false,
driving: false
}
// Make styled elements to trigger file input
$(document).on('change', '.btn-file :file', function() {
var input = $(this),
numFiles = input.get(0).files ? input.get(0).files.length : 1,
label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
input.trigger('fileselect', [numFiles, label]);
});
function checkForward() {
// Is all upload done and we can go to the next page?
if(state.medical && state.driving) {
window.location = window.nextURL;
}
}
function doUpload(name) {
var baseElem = $("#upload-" + name);
if(baseElem.length != 1) {
throw new Error("Wooops, bad DOM tree");
}
function onStart() {
baseElem.find(".progress").show();
baseElem.find(".error").hide();
baseElem.find(".success").hide();
}
function onDone(result, data) {
baseElem.find(".progress").hide();
if(data.result.status == "ok") {
// All ok, check if we can proceed
baseElem.find(".success").show();
state[name] = true;
checkForward();
} else {
// Server responded us it didn't like the file and gave a specific error message
var msg = data.result.message;
baseElem.find(".error-message").text(msg);
baseElem.find(".error").show();
state[name] = false;
}
}
function onError(result, data) {
baseElem.find(".progress").hide();
baseElem.find(".error-message").text("Upload could not be completed. Please contact the support.");
baseElem.find(".error").show();
state[name] = false;
}
function onProgress(e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
baseElem.find(".progress-bar").css("width", progress + "%");
}
function onPreview(e, data) {
var canvas = data.files[0].preview;
var dataURL = canvas.toDataURL();
baseElem.find(".preview").css("background-image", 'url(' + dataURL +')');
baseElem.find(".preview").css({width: canvas.width, height: canvas.height});
baseElem.find(".preview").show();
}
var upload = baseElem.find('.file-select');
upload.fileupload({
dataType: 'json',
// Enable image resizing, except for Android and Opera,
// which actually support image resizing, but fail to
// send Blob objects via XHR requests:
// disableImageResize: /Android(?!.*Chrome)|Opera/
// .test(window.navigator && navigator.userAgent),
disableImageResize: false,
imageMaxWidth: 1024,
imageMaxHeight: 1024,
imageCrop: false, // Force cropped images,
previewMaxWidth: 128,
previewMaxHeight: 128,
maxFileSize: 7*1024*1024
});
upload.bind("fileuploaddone", onDone);
upload.bind("fileuploadstart", onStart);
upload.bind("fileuploadfail", onError);
upload.bind("fileuploadprogress", onProgress);
upload.bind('fileuploadprocessalways', onPreview);
}
$(document).ready(function() {
doUpload("medical");
doUpload("driving");
});
</script>
{% endblock %}
次に、ペイロードをデコードし、ユーザーがランダムなファイルではなく画像をアップロードしたことを確認する単純なサーバー側の Pyramid ビュー。結果は、JavaScript がデコードできる JSON 応答です。
@view_config(route_name='upload_target', renderer='json')
def upload_target(request):
"""AJAX upload of driving license and medical images."""
if "medical" in request.params:
license = "medical"
files = request.params["medical"]
elif "driving" in request.params:
license = "driving"
files = request.params["driving"]
else:
raise RuntimeError("Unsupported upload type")
# # TODO: use chunks, do not buffer 100%
# path = user.prepare_upload_path()
storage = io.open(where_to_save, "wb")
fp = files.file
# Now we test for a valid image upload
image_test = imghdr.what(fp)
if image_test == None:
return {"status": "fail", "message": "Only JPEG and PNG image file upload supported."}
fp.seek(0)
data = fp.read()
assert len(data) > 0
storage.write(data)
storage.close()
return {"status": "ok"}