@@ -293,16 +293,32 @@ def upload_from_pc(request): | |||
""" | |||
form = request.form | |||
upload = request.files["file"] | |||
# Make a temp filename for it. | |||
filetype = upload.filename.rsplit(".", 1)[1] | |||
tempfile = "{}/rophako-photo-{}.{}".format(config.TEMPDIR, int(time.time()), filetype) | |||
logger.debug("Save incoming photo to: {}".format(tempfile)) | |||
upload.save(tempfile) | |||
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] | |||
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. | |||
status = process_photo(form, tempfile) | |||
if not status["success"]: | |||
return status | |||
# Multi upload? | |||
if count > 1: | |||
status["multi"] = True | |||
else: | |||
status["multi"] = False | |||
# All good so far. Process the photo. | |||
return process_photo(form, tempfile) | |||
return status | |||
def upload_from_www(form): | |||
@@ -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,9 +91,24 @@ def upload(): | |||
# How'd it go? | |||
if result["success"] is not True: | |||
flash("The upload has failed: {}".format(result["error"])) | |||
return redirect(url_for(".upload")) | |||
return redirect(url_for(".crop", photo=result["photo"])) | |||
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. | |||
g.info["album_list"] = [ | |||
@@ -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.""" | |||
@@ -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(); | |||
@@ -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 %} | |||
@@ -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(); | |||
}); |
@@ -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; | |||