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 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. # Make a temp filename for it.
filetype = upload.filename.rsplit(".", 1)[1] filetype = upload.filename.rsplit(".", 1)[-1]
tempfile = "{}/rophako-photo-{}.{}".format(config.TEMPDIR, int(time.time()), filetype) if not allowed_filetype(upload.filename):
logger.debug("Save incoming photo to: {}".format(tempfile)) return dict(success=False, error="Unsupported file extension.")
upload.save(tempfile)
# All good so far. Process the photo. tempfile = "{}/rophako-photo-{}.{}".format(config.TEMPDIR, int(time.time()), filetype)
return process_photo(form, tempfile) logger.debug("Save incoming photo to: {}".format(tempfile))
upload.save(tempfile)
# All good so far. Process the photo.
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): 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.user as User
import rophako.model.photo as Photo 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 rophako.log import logger
from config import * from config import *
@ -68,7 +68,15 @@ def upload():
"""Upload a photo.""" """Upload a photo."""
if request.method == "POST": 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 result = None
location = request.form.get("location") location = request.form.get("location")
if location == "pc": if location == "pc":
@ -83,9 +91,24 @@ def upload():
# How'd it go? # How'd it go?
if result["success"] is not True: if result["success"] is not True:
flash("The upload has failed: {}".format(result["error"])) if is_ajax:
return redirect(url_for(".upload")) return ajax_response(False, result["error"])
return redirect(url_for(".crop", photo=result["photo"])) 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. # Get the list of available albums.
g.info["album_list"] = [ g.info["album_list"] = [

View File

@ -10,6 +10,7 @@ import re
import importlib import importlib
import smtplib import smtplib
import markdown import markdown
import json
from rophako.log import logger from rophako.log import logger
from config import * from config import *
@ -45,6 +46,15 @@ def admin_required(f):
return decorated_function 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): def template(name, **kwargs):
"""Render a template to the browser.""" """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 You can upload a photo from your computer or by pasting in the URL to a photo
somewhere else on the Internet. 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() }}"> <input type="hidden" name="token" value="{{ csrf_token() }}">
<fieldset> <fieldset>
@ -22,7 +22,10 @@ somewhere else on the Internet.
<div id="pic-pc" class="location-div"> <div id="pic-pc" class="location-div">
<strong>Upload a picture from my computer</strong><br> <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>
<div id="pic-www" class="location-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 Only jpeg, gif and png images are supported. There is no maximum file size
limit, but be reasonable. limit, but be reasonable.
</fieldset> </fieldset>
<p>
<fieldset> <fieldset>
<legend>Photo Options</legend> <legend>Photo Options</legend>
@ -58,13 +62,23 @@ somewhere else on the Internet.
</fieldset> </fieldset>
<p> <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> </form>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script type="text/javascript" src="/rophako/multiupload.js"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
$("#pic-www").hide(); $("#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_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.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.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> <li><a href="{{ url_for('photo.delete', key=photo['key']) }}">Delete this photo</a></li>
</ul> </ul>
{% endif %} {% 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; 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 */ /* Photo Grids: see http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
ul.photo-grid { ul.photo-grid {
list-style: none; list-style: none;