Add multiple photo upload support

This commit is contained in:
Noah 2014-06-20 18:32:05 -07:00
parent dc87792545
commit a682ea17c1
7 changed files with 292 additions and 16 deletions

View File

@ -293,16 +293,32 @@ def upload_from_pc(request):
"""
form = request.form
upload = request.files["file"]
count = 0
status = None
for upload in request.files.getlist("file"):
count += 1
# Make a temp filename for it.
filetype = upload.filename.rsplit(".", 1)[1]
filetype = upload.filename.rsplit(".", 1)[-1]
if not allowed_filetype(upload.filename):
return dict(success=False, error="Unsupported file extension.")
tempfile = "{}/rophako-photo-{}.{}".format(config.TEMPDIR, int(time.time()), filetype)
logger.debug("Save incoming photo to: {}".format(tempfile))
upload.save(tempfile)
# All good so far. Process the photo.
return process_photo(form, tempfile)
status = process_photo(form, tempfile)
if not status["success"]:
return status
# Multi upload?
if count > 1:
status["multi"] = True
else:
status["multi"] = False
return status
def upload_from_www(form):

View File

@ -6,7 +6,7 @@ from flask import Blueprint, g, request, redirect, url_for, session, flash
import rophako.model.user as User
import rophako.model.photo as Photo
from rophako.utils import template, pretty_time, login_required
from rophako.utils import template, pretty_time, login_required, ajax_response
from rophako.log import logger
from config import *
@ -68,7 +68,15 @@ def upload():
"""Upload a photo."""
if request.method == "POST":
# We're posting the upload. What source is the pic from?
# We're posting the upload.
# Is this an ajax post or a direct post?
is_ajax = request.form.get("__ajax", "false") == "true"
# Album name.
album = request.form.get("album") or request.form.get("new-album")
# What source is the pic from?
result = None
location = request.form.get("location")
if location == "pc":
@ -83,8 +91,23 @@ def upload():
# How'd it go?
if result["success"] is not True:
if is_ajax:
return ajax_response(False, result["error"])
else:
flash("The upload has failed: {}".format(result["error"]))
return redirect(url_for(".upload"))
# Good!
if is_ajax:
# Was it a multiple upload?
if result["multi"]:
return ajax_response(True, url_for(".album_index", name=album))
else:
return ajax_response(True, url_for(".crop", photo=result["photo"]))
else:
if result["multi"]:
return redirect(url_for(".album_index", name=album))
else:
return redirect(url_for(".crop", photo=result["photo"]))
# Get the list of available albums.

View File

@ -10,6 +10,7 @@ import re
import importlib
import smtplib
import markdown
import json
from rophako.log import logger
from config import *
@ -45,6 +46,15 @@ def admin_required(f):
return decorated_function
def ajax_response(status, msg):
"""Return a standard JSON response."""
status = "ok" if status else "error"
return json.dumps(dict(
status=status,
msg=msg,
))
def template(name, **kwargs):
"""Render a template to the browser."""

View File

@ -7,7 +7,7 @@
You can upload a photo from your computer or by pasting in the URL to a photo
somewhere else on the Internet.
<form name="upload" action="{{ url_for('photo.upload') }}" method="POST" enctype="multipart/form-data">
<form id="upload-form" action="{{ url_for('photo.upload') }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<fieldset>
@ -22,7 +22,10 @@ somewhere else on the Internet.
<div id="pic-pc" class="location-div">
<strong>Upload a picture from my computer</strong><br>
<input type="file" size="30" name="file">
<input type="file" size="30" name="file" id="file-picker" accept="image/*" multiple><p>
<strong>Or, drag images here:</strong><br>
<div id="dropbox" class="photo-upload-dropbox">Drag and drop images into this box</div>
</div>
<div id="pic-www" class="location-div">
@ -34,6 +37,7 @@ somewhere else on the Internet.
Only jpeg, gif and png images are supported. There is no maximum file size
limit, but be reasonable.
</fieldset>
<p>
<fieldset>
<legend>Photo Options</legend>
@ -58,13 +62,23 @@ somewhere else on the Internet.
</fieldset>
<p>
<button type="submit">Upload Picture</button>
<fieldset id="upload-progress" style="display: none">
<legend>Upload Progress</legend>
<div class="upload-trough">
<div id="upload-progress-bar" class="upload-progress-bar"></div>
</div>
</fieldset>
<p>
<button type="submit" id="upload-button">Upload Picture</button>
</form>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="/rophako/multiupload.js"></script>
<script>
$(document).ready(function() {
$("#pic-www").hide();

View File

@ -42,6 +42,7 @@ Uploaded by {{ author["name"] }} on {{ photo["pretty_time"] }}.
<li><a href="{{ url_for('photo.set_cover', album=photo['album'], key=photo['key']) }}">Set Album Cover</a></li>
<li><a href="{{ url_for('photo.set_profile', key=photo['key']) }}">Set as my Profile Picture</a></li>
<li><a href="{{ url_for('photo.edit', key=photo['key']) }}">Edit this photo</a></li>
<li><a href="{{ url_for('photo.crop', photo=photo['key']) }}">Change Thumbnail</a></li>
<li><a href="{{ url_for('photo.delete', key=photo['key']) }}">Delete this photo</a></li>
</ul>
{% endif %}

View File

@ -0,0 +1,191 @@
/* rophako cms
-----------
HTML5 multi-upload script for the photo albums.
*/
function RophakoUpload() {
var self = this;
// Constants
this.MAX_UPLOAD_FILE_SIZE = 1024*1024; // 1 MB
this.UPLOAD_URL = "/photos/upload";
this.NEXT_URL = "/files/";
// List of pending files to handle when the Upload button is finally clicked.
this.PENDING_FILES = [];
this.ready = function() {
// Set up the drag/drop zone.
self.initDropbox();
// Set up the handler for the file input box.
$("#file-picker").on("change", function() {
self.handleFiles(this.files);
});
// Handle the submit button.
$("#upload-button").on("click", function(e) {
// If the user has JS disabled, none of this code is running but the
// file multi-upload input box should still work. In this case they'll
// just POST to the upload endpoint directly. However, with JS we'll do
// the POST using ajax and then redirect them ourself when done.
e.preventDefault();
self.doUpload();
});
};
this.doUpload = function() {
$("#upload-progress").show();
var $progressBar = $("#upload-progress-bar");
// Gray out the form.
$("#upload-button").attr("disabled", "disabled");
// Initialize the progress bar.
$progressBar.css({"width": "0%"});
// Collect the form data.
fd = self.collectFormData();
// Attach the files.
for (var i = 0, ie = self.PENDING_FILES.length; i < ie; i++) {
// Collect the other form data.
fd.append("file", self.PENDING_FILES[i]);
}
// Inform the back-end that we're doing this over ajax.
fd.append("__ajax", "true");
var xhr = $.ajax({
xhr: function() {
var xhrobj = $.ajaxSettings.xhr();
if (xhrobj.upload) {
xhrobj.upload.addEventListener("progress", function(event) {
var percent = 0;
var position = event.loaded || event.position;
var total = event.total;
if (event.lengthComputable) {
percent = Math.ceil(position / total * 100);
}
// Set the progress bar.
$progressBar.css({"width": percent + "%"});
$progressBar.text(percent + "%");
}, false)
}
return xhrobj;
},
url: self.UPLOAD_URL,
method: "POST",
contentType: false, //"multipart/form-data",
processData: false,
dataType: "json",
cache: false,
data: fd,
success: function(data) {
console.log(data);
$progressBar.css({"width": "100%"});
// How'd it go?
if (data.status === "error") {
// Uh-oh.
window.alert(data.msg);
$("#upload-button").removeAttr("disabled");
return;
}
else {
// Ok!
window.location = data.msg;
}
},
});
};
this.collectFormData = function() {
// Go through all the form fields and collect their names/values.
var fd = new FormData();
$("#upload-form :input").each(function() {
var $this = $(this);
var name = $this.attr("name");
var type = $this.attr("type") || "";
var value = $this.val();
// No name = no care.
if (name === undefined) {
return;
}
// Skip the file upload box for now.
if (type === "file") {
return;
}
// Checkboxes? Only add their value if they're checked.
if (type === "checkbox" || type === "radio") {
if (!$this.is(":checked")) {
return;
}
}
fd.append(name, value);
});
return fd;
};
this.handleFiles = function(files) {
// Add them to the pending files list.
for (var i = 0, ie = files.length; i < ie; i++) {
self.PENDING_FILES.push(files[i]);
}
};
this.initDropbox = function() {
var $dropbox = $("#dropbox");
// On drag enter...
$dropbox.on("dragenter", function(e) {
e.stopPropagation();
e.preventDefault();
$(this).addClass("active");
});
// On drag over...
$dropbox.on("dragover", function(e) {
e.stopPropagation();
e.preventDefault();
});
// On drop...
$dropbox.on("drop", function(e) {
e.preventDefault();
$(this).removeClass("active");
// Get the files.
var files = e.originalEvent.dataTransfer.files;
self.handleFiles(files);
// Update the display to acknowledge the number of pending files.
$dropbox.text(self.PENDING_FILES.length + " files ready for upload!");
});
// If the files are dropped outside of the drop zone, the browser will
// redirect to show the files in the window. To avoid that we can prevent
// the 'drop' event on the document.
function stopDefault(e) {
e.stopPropagation();
e.preventDefault();
}
$(document).on("dragenter", stopDefault);
$(document).on("dragover", stopDefault);
$(document).on("drop", stopDefault);
};
};
$(document).ready(function() {
new RophakoUpload().ready();
});

View File

@ -130,6 +130,27 @@ table.table td {
padding: 4px;
}
/* Photo Upload page */
.photo-upload-dropbox {
border: 2px dashed #000000;
padding: 40px;
}
.photo-upload-dropbox.active {
border: 4px dashed #FF0000;
}
.upload-trough {
position: relative;
border: 1px solid #000000;
width: 100%;
height: 28px;
}
.upload-progress-bar {
position: relative;
width: 0%;
height: 100%;
background-color: #FF9900;
}
/* Photo Grids: see http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
ul.photo-grid {
list-style: none;