first pass of multi-uploader

This commit is contained in:
Ben Margolis 2014-06-15 22:35:01 -07:00
parent 16ca198377
commit 138b3ba285
10 changed files with 1843 additions and 1 deletions

View file

@ -295,6 +295,7 @@ button.icon{
> li{
position:relative;
overflow:hidden;
background-color:white;
padding:1em 1.5em;
margin-bottom:1em;

View file

@ -14,6 +14,15 @@ def get_image_form():
widgets={'file': forms.FileInput()})
def get_image_form_for_multi():
return modelform_factory(
get_image_model(),
# set the 'file' widget to a FileInput rather than the default ClearableFileInput
# so that when editing, we don't get the 'currently: ...' banner which is
# a bit pointless here
exclude=('file',))
class ImageInsertionForm(forms.Form):
"""
Form for selecting parameters of the image (e.g. format) prior to insertion

View file

@ -0,0 +1,40 @@
$(function () {
function process_result(data) {
var result = $.parseJSON(data);
if (result.success) {
$('li#image-'+result.image_id).slideUp(function() { $(this).remove(); });
}
}
$('#fileupload').fileupload({
dataType: 'html',
sequentialUploads: true,
done: function (e, data) {
var im_li = $(data.result);
im_li.find('form').each(function() {
var jform = $(this);
jform.submit(function(event) { //convert save to an ajax call
event.preventDefault();
$.post(this.action, $(this).serialize(), process_result);
});
jform.find('a').each(function(){ //convert delete to an ajax call
$(this).click(function(event) {
event.preventDefault();
$.post(this.href, jform.serialize(), process_result);
});
});
jform.find('#id_'+ im_li.attr('id') +'-tags').tagit(window.tagit_opts);
});
im_li
$("#image-forms").append(im_li);
}
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,214 @@
/*
* jQuery Iframe Transport Plugin 1.8.2
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
/* global define, window, document */
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define(['jquery'], factory);
} else {
// Browser globals:
factory(window.jQuery);
}
}(function ($) {
'use strict';
// Helper variable to create unique names for the transport iframes:
var counter = 0;
// The iframe transport accepts four additional options:
// options.fileInput: a jQuery collection of file input fields
// options.paramName: the parameter name for the file form data,
// overrides the name property of the file input field(s),
// can be a string or an array of strings.
// options.formData: an array of objects with name and value properties,
// equivalent to the return data of .serializeArray(), e.g.:
// [{name: 'a', value: 1}, {name: 'b', value: 2}]
// options.initialIframeSrc: the URL of the initial iframe src,
// by default set to "javascript:false;"
$.ajaxTransport('iframe', function (options) {
if (options.async) {
// javascript:false as initial iframe src
// prevents warning popups on HTTPS in IE6:
/*jshint scripturl: true */
var initialIframeSrc = options.initialIframeSrc || 'javascript:false;',
/*jshint scripturl: false */
form,
iframe,
addParamChar;
return {
send: function (_, completeCallback) {
form = $('<form style="display:none;"></form>');
form.attr('accept-charset', options.formAcceptCharset);
addParamChar = /\?/.test(options.url) ? '&' : '?';
// XDomainRequest only supports GET and POST:
if (options.type === 'DELETE') {
options.url = options.url + addParamChar + '_method=DELETE';
options.type = 'POST';
} else if (options.type === 'PUT') {
options.url = options.url + addParamChar + '_method=PUT';
options.type = 'POST';
} else if (options.type === 'PATCH') {
options.url = options.url + addParamChar + '_method=PATCH';
options.type = 'POST';
}
// IE versions below IE8 cannot set the name property of
// elements that have already been added to the DOM,
// so we set the name along with the iframe HTML markup:
counter += 1;
iframe = $(
'<iframe src="' + initialIframeSrc +
'" name="iframe-transport-' + counter + '"></iframe>'
).bind('load', function () {
var fileInputClones,
paramNames = $.isArray(options.paramName) ?
options.paramName : [options.paramName];
iframe
.unbind('load')
.bind('load', function () {
var response;
// Wrap in a try/catch block to catch exceptions thrown
// when trying to access cross-domain iframe contents:
try {
response = iframe.contents();
// Google Chrome and Firefox do not throw an
// exception when calling iframe.contents() on
// cross-domain requests, so we unify the response:
if (!response.length || !response[0].firstChild) {
throw new Error();
}
} catch (e) {
response = undefined;
}
// The complete callback returns the
// iframe content document as response object:
completeCallback(
200,
'success',
{'iframe': response}
);
// Fix for IE endless progress bar activity bug
// (happens on form submits to iframe targets):
$('<iframe src="' + initialIframeSrc + '"></iframe>')
.appendTo(form);
window.setTimeout(function () {
// Removing the form in a setTimeout call
// allows Chrome's developer tools to display
// the response result
form.remove();
}, 0);
});
form
.prop('target', iframe.prop('name'))
.prop('action', options.url)
.prop('method', options.type);
if (options.formData) {
$.each(options.formData, function (index, field) {
$('<input type="hidden"/>')
.prop('name', field.name)
.val(field.value)
.appendTo(form);
});
}
if (options.fileInput && options.fileInput.length &&
options.type === 'POST') {
fileInputClones = options.fileInput.clone();
// Insert a clone for each file input field:
options.fileInput.after(function (index) {
return fileInputClones[index];
});
if (options.paramName) {
options.fileInput.each(function (index) {
$(this).prop(
'name',
paramNames[index] || options.paramName
);
});
}
// Appending the file input fields to the hidden form
// removes them from their original location:
form
.append(options.fileInput)
.prop('enctype', 'multipart/form-data')
// enctype must be set as encoding for IE:
.prop('encoding', 'multipart/form-data');
// Remove the HTML5 form attribute from the input(s):
options.fileInput.removeAttr('form');
}
form.submit();
// Insert the file input fields at their original location
// by replacing the clones with the originals:
if (fileInputClones && fileInputClones.length) {
options.fileInput.each(function (index, input) {
var clone = $(fileInputClones[index]);
// Restore the original name and form properties:
$(input)
.prop('name', clone.prop('name'))
.attr('form', clone.attr('form'));
clone.replaceWith(input);
});
}
});
form.append(iframe).appendTo(document.body);
},
abort: function () {
if (iframe) {
// javascript:false as iframe src aborts the request
// and prevents warning popups on HTTPS in IE6.
// concat is used to avoid the "Script URL" JSLint error:
iframe
.unbind('load')
.prop('src', initialIframeSrc);
}
if (form) {
form.remove();
}
}
};
}
});
// The iframe transport returns the iframe content document as response.
// The following adds converters from iframe to text, json, html, xml
// and script.
// Please note that the Content-Type for JSON responses has to be text/plain
// or text/html, if the browser doesn't include application/json in the
// Accept header, else IE will show a download dialog.
// The Content-Type for XML responses on the other hand has to be always
// application/xml or text/xml, so IE properly parses the XML response.
// See also
// https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation
$.ajaxSetup({
converters: {
'iframe text': function (iframe) {
return iframe && $(iframe[0].body).text();
},
'iframe json': function (iframe) {
return iframe && $.parseJSON($(iframe[0].body).text());
},
'iframe html': function (iframe) {
return iframe && $(iframe[0].body).html();
},
'iframe xml': function (iframe) {
var xmlDoc = iframe && iframe[0];
return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc :
$.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) ||
$(xmlDoc.body).html());
},
'iframe script': function (iframe) {
return iframe && $.globalEval($(iframe[0].body).text());
}
}
});
}));

View file

@ -0,0 +1,39 @@
{% extends "wagtailadmin/base.html" %}
{% load image_tags %}
{% load i18n %}
{% block titletag %}{% trans "Add multiple images" %}{% endblock %}
{% block bodyclass %}menu-images{% endblock %}
{% block extra_css %}
{% include "wagtailadmin/shared/tag_field_css.html" %}
{% endblock %}
{% block extra_js %}
<script src="{{ STATIC_URL }}wagtailimages/js/vendor/jquery.iframe-transport.js"></script>
<script src="{{ STATIC_URL }}wagtailimages/js/vendor/jquery.fileupload.js"></script>
<script src="{{ STATIC_URL }}wagtailimages/js/add-multiple.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/tag-it.js"></script>
{% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %}
<script>
window.tagit_opts = {
autocomplete: {source: "{{ autocomplete_url|addslashes }}"}
};
</script>
{% endblock %}
{% block content %}
{% trans "Add image" as add_str %}
{% include "wagtailadmin/shared/header.html" with title=add_str icon="image" %}
<div class="nice-padding">
<form action="{% url 'wagtailimages_add_multiple' %}" method="POST" enctype="multipart/form-data">
<input id="fileupload" type="file" name="files[]" data-url="{% url 'wagtailimages_add_multiple' %}" multiple>
{% csrf_token %}
</form>
<ul class="multiple" id="image-forms">
</ul>
</div>
{% endblock %}

View file

@ -0,0 +1,4 @@
{
"image_id": {{ image_id }},
"success": {% if success %}true{% else %}false{% endif %}
}

View file

@ -0,0 +1,18 @@
{% load i18n image_tags %}
<li id="image-{{ image.id }}">
<form action="{% url 'wagtailimages_edit_multiple' image.id %}" method="POST" enctype="multipart/form-data">
<ul class="fields col7">
{% csrf_token %}
{% for field in form %}
{% include "wagtailadmin/shared/field_as_li.html" %}
{% endfor %}
<li>
<input type="submit" value="{% trans 'Save' %}" />
<a href="{% url 'wagtailimages_delete_multiple' image.id %}" class="button button-secondary no">{% trans "Cancel upload" %}</a>
</li>
</ul>
</form>
<div class="col5">
{% image image max-800x600 %}
</div>
</li>

View file

@ -1,5 +1,5 @@
from django.conf.urls import url
from wagtail.wagtailimages.views import images, chooser
from wagtail.wagtailimages.views import images, chooser, multiple
urlpatterns = [
url(r'^$', images.index, name='wagtailimages_index'),
@ -7,6 +7,10 @@ urlpatterns = [
url(r'^(\d+)/delete/$', images.delete, name='wagtailimages_delete_image'),
url(r'^add/$', images.add, name='wagtailimages_add_image'),
url(r'^multiple/add/$', multiple.add, name='wagtailimages_add_multiple'),
url(r'^multiple/(\d+)/$', multiple.edit, name='wagtailimages_edit_multiple'),
url(r'^multiple/(\d+)/delete$', multiple.delete, name='wagtailimages_delete_multiple'),
url(r'^chooser/$', chooser.chooser, name='wagtailimages_chooser'),
url(r'^chooser/(\d+)/$', chooser.image_chosen, name='wagtailimages_image_chosen'),
url(r'^chooser/upload/$', chooser.chooser_upload, name='wagtailimages_chooser_upload'),

View file

@ -0,0 +1,87 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib.auth.decorators import permission_required
from django.core.exceptions import PermissionDenied
from django.utils.translation import ugettext as _
from django.views.decorators.vary import vary_on_headers
from django.forms.models import modelformset_factory
from django.template.loader import render_to_string
from django.http import HttpResponse
from wagtail.wagtailadmin.forms import SearchForm
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.forms import get_image_form_for_multi
import json
@permission_required('wagtailimages.add_image')
@vary_on_headers('X-Requested-With')
def add(request):
ImageForm = get_image_form_for_multi()
ImageModel = get_image_model()
if request.POST and request.is_ajax():
if not request.FILES:
return HttpResponseBadRequest('Must upload a file')
else:
image = ImageModel(uploaded_by_user=request.user, title=request.FILES['files[]'].name, file=request.FILES['files[]'])
image.save()
form = ImageForm(instance=image, prefix='image-%d'%image.id)
return render(request, 'wagtailimages/multiple/edit.html', {
'image': image,
'form': form
})
else:
pass
return render(request, "wagtailimages/multiple/add.html", {})
@permission_required('wagtailadmin.access_admin') # more specific permission tests are applied within the view
def edit(request, image_id, callback=None):
Image = get_image_model()
ImageForm = get_image_form_for_multi()
image = get_object_or_404(Image, id=image_id)
if not image.is_editable_by_user(request.user):
raise PermissionDenied
if request.POST:
form = ImageForm(request.POST, request.FILES, instance=image, prefix='image-'+image_id)
if form.is_valid():
form.save()
return HttpResponse(render_to_string("wagtailimages/multiple/confirmation.json", {
'success': True,
'image_id': image_id
}))
else:
pass
return HttpResponse(render_to_string("wagtailimages/multiple/confirmation.json", {
'success': False,
'image_id': image_id
}))
@permission_required('wagtailadmin.access_admin') # more specific permission tests are applied within the view
def delete(request, image_id):
image = get_object_or_404(get_image_model(), id=image_id)
if not image.is_editable_by_user(request.user):
raise PermissionDenied
if request.POST:
image.delete()
return HttpResponse(render_to_string("wagtailimages/multiple/confirmation.json", {
'success': True,
'image_id': image_id
}))
else:
return HttpResponse(render_to_string("wagtailimages/multiple/confirmation.json", {
'success': False,
'image_id': image_id
}))