mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-13 09:43:10 +00:00
Upload multiple documents at once
Functionality from the multiple image uploader was used.
This commit is contained in:
parent
ff9289fdb0
commit
f0095b5882
12 changed files with 2467 additions and 2 deletions
|
|
@ -1,5 +1,5 @@
|
|||
from django.conf.urls import url
|
||||
from wagtail.wagtaildocs.views import documents, chooser
|
||||
from wagtail.wagtaildocs.views import documents, chooser, multiple
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -8,6 +8,10 @@ urlpatterns = [
|
|||
url(r'^edit/(\d+)/$', documents.edit, name='edit'),
|
||||
url(r'^delete/(\d+)/$', documents.delete, name='delete'),
|
||||
|
||||
url(r'^multiple/add/$', multiple.add, name='add_multiple'),
|
||||
url(r'^multiple/(\d+)/$', multiple.edit, name='edit_multiple'),
|
||||
url(r'^multiple/(\d+)/delete/$', multiple.delete, name='delete_multiple'),
|
||||
|
||||
url(r'^chooser/$', chooser.chooser, name='chooser'),
|
||||
url(r'^chooser/(\d+)/$', chooser.document_chosen, name='document_chosen'),
|
||||
url(r'^chooser/upload/$', chooser.chooser_upload, name='chooser_upload'),
|
||||
|
|
|
|||
|
|
@ -12,3 +12,13 @@ def get_document_form(model):
|
|||
'tags': widgets.AdminTagWidget,
|
||||
'file': forms.FileInput()
|
||||
})
|
||||
|
||||
|
||||
def get_document_multi_form(model):
|
||||
return modelform_factory(
|
||||
model,
|
||||
fields=['title', 'tags'],
|
||||
widgets={
|
||||
'tags': widgets.AdminTagWidget,
|
||||
'file': forms.FileInput()
|
||||
})
|
||||
|
|
|
|||
147
wagtail/wagtaildocs/static_src/wagtaildocs/js/add-multiple.js
Normal file
147
wagtail/wagtaildocs/static_src/wagtaildocs/js/add-multiple.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
$(function() {
|
||||
// Redirect users that don't support filereader
|
||||
if (!$('html').hasClass('filereader')) {
|
||||
document.location.href = window.fileupload_opts.simple_upload_url;
|
||||
return false;
|
||||
}
|
||||
|
||||
// prevents browser default drag/drop
|
||||
$(document).bind('drop dragover', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$('#fileupload').fileupload({
|
||||
dataType: 'html',
|
||||
sequentialUploads: true,
|
||||
dropZone: $('.drop-zone'),
|
||||
add: function(e, data) {
|
||||
var $this = $(this);
|
||||
var that = $this.data('blueimp-fileupload') || $this.data('fileupload')
|
||||
var li = $($('#upload-list-item').html()).addClass('upload-uploading')
|
||||
var options = that.options;
|
||||
|
||||
$('#upload-list').append(li);
|
||||
data.context = li;
|
||||
|
||||
data.process(function() {
|
||||
return $this.fileupload('process', data);
|
||||
}).always(function() {
|
||||
data.context.removeClass('processing');
|
||||
data.context.find('.left').each(function(index, elm) {
|
||||
$(elm).append(escapeHtml(data.files[index].name));
|
||||
});
|
||||
|
||||
}).done(function() {
|
||||
data.context.find('.start').prop('disabled', false);
|
||||
if ((that._trigger('added', e, data) !== false) &&
|
||||
(options.autoUpload || data.autoUpload) &&
|
||||
data.autoUpload !== false) {
|
||||
data.submit()
|
||||
}
|
||||
}).fail(function() {
|
||||
if (data.files.error) {
|
||||
data.context.each(function(index) {
|
||||
var error = data.files[index].error;
|
||||
if (error) {
|
||||
$(this).find('.error_messages').text(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
processfail: function(e, data) {
|
||||
var itemElement = $(data.context);
|
||||
itemElement.removeClass('upload-uploading').addClass('upload-failure');
|
||||
},
|
||||
|
||||
progress: function(e, data) {
|
||||
if (e.isDefaultPrevented()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var progress = Math.floor(data.loaded / data.total * 100);
|
||||
data.context.each(function() {
|
||||
$(this).find('.progress').addClass('active').attr('aria-valuenow', progress).find('.bar').css(
|
||||
'width',
|
||||
progress + '%'
|
||||
).html(progress + '%');
|
||||
});
|
||||
},
|
||||
|
||||
progressall: function(e, data) {
|
||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
$('#overall-progress').addClass('active').attr('aria-valuenow', progress).find('.bar').css(
|
||||
'width',
|
||||
progress + '%'
|
||||
).html(progress + '%');
|
||||
|
||||
if (progress >= 100) {
|
||||
$('#overall-progress').removeClass('active').find('.bar').css('width', '0%');
|
||||
}
|
||||
},
|
||||
|
||||
done: function(e, data) {
|
||||
var itemElement = $(data.context);
|
||||
var response = $.parseJSON(data.result);
|
||||
|
||||
if (response.success) {
|
||||
itemElement.addClass('upload-success')
|
||||
|
||||
$('.right', itemElement).append(response.form);
|
||||
|
||||
// run tagit enhancement
|
||||
$('.tag_field input', itemElement).tagit(window.tagit_opts);
|
||||
} else {
|
||||
itemElement.addClass('upload-failure');
|
||||
$('.right .error_messages', itemElement).append(response.error_message);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
fail: function(e, data) {
|
||||
var itemElement = $(data.context);
|
||||
itemElement.addClass('upload-failure');
|
||||
},
|
||||
|
||||
always: function(e, data) {
|
||||
var itemElement = $(data.context);
|
||||
itemElement.removeClass('upload-uploading').addClass('upload-complete');
|
||||
}
|
||||
});
|
||||
|
||||
// ajax-enhance forms added on done()
|
||||
$('#upload-list').on('submit', 'form', function(e) {
|
||||
var form = $(this);
|
||||
var itemElement = form.closest('#upload-list > li');
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
$.post(this.action, form.serialize(), function(data) {
|
||||
if (data.success) {
|
||||
itemElement.slideUp(function() {$(this).remove()});
|
||||
} else {
|
||||
form.replaceWith(data.form);
|
||||
|
||||
// run tagit enhancement on new form
|
||||
$('.tag_field input', form).tagit(window.tagit_opts);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#upload-list').on('click', '.delete', function(e) {
|
||||
var form = $(this).closest('form');
|
||||
var itemElement = form.closest('#upload-list > li');
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
var CSRFToken = $('input[name="csrfmiddlewaretoken"]', form).val();
|
||||
|
||||
$.post(this.href, {csrfmiddlewaretoken: CSRFToken}, function(data) {
|
||||
if (data.success) {
|
||||
itemElement.slideUp(function() {$(this).remove()});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
172
wagtail/wagtaildocs/static_src/wagtaildocs/js/vendor/jquery.fileupload-process.js
vendored
Normal file
172
wagtail/wagtaildocs/static_src/wagtaildocs/js/vendor/jquery.fileupload-process.js
vendored
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* jQuery File Upload Processing Plugin 1.3.0
|
||||
* https://github.com/blueimp/jQuery-File-Upload
|
||||
*
|
||||
* Copyright 2012, Sebastian Tschan
|
||||
* https://blueimp.net
|
||||
*
|
||||
* Licensed under the MIT license:
|
||||
* http://www.opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
/* jshint nomen:false */
|
||||
/* global define, window */
|
||||
|
||||
(function (factory) {
|
||||
'use strict';
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// Register as an anonymous AMD module:
|
||||
define([
|
||||
'jquery',
|
||||
'./jquery.fileupload'
|
||||
], factory);
|
||||
} else {
|
||||
// Browser globals:
|
||||
factory(
|
||||
window.jQuery
|
||||
);
|
||||
}
|
||||
}(function ($) {
|
||||
'use strict';
|
||||
|
||||
var originalAdd = $.blueimp.fileupload.prototype.options.add;
|
||||
|
||||
// The File Upload Processing plugin extends the fileupload widget
|
||||
// with file processing functionality:
|
||||
$.widget('blueimp.fileupload', $.blueimp.fileupload, {
|
||||
|
||||
options: {
|
||||
// The list of processing actions:
|
||||
processQueue: [
|
||||
/*
|
||||
{
|
||||
action: 'log',
|
||||
type: 'debug'
|
||||
}
|
||||
*/
|
||||
],
|
||||
add: function (e, data) {
|
||||
var $this = $(this);
|
||||
data.process(function () {
|
||||
return $this.fileupload('process', data);
|
||||
});
|
||||
originalAdd.call(this, e, data);
|
||||
}
|
||||
},
|
||||
|
||||
processActions: {
|
||||
/*
|
||||
log: function (data, options) {
|
||||
console[options.type](
|
||||
'Processing "' + data.files[data.index].name + '"'
|
||||
);
|
||||
}
|
||||
*/
|
||||
},
|
||||
|
||||
_processFile: function (data, originalData) {
|
||||
var that = this,
|
||||
dfd = $.Deferred().resolveWith(that, [data]),
|
||||
chain = dfd.promise();
|
||||
this._trigger('process', null, data);
|
||||
$.each(data.processQueue, function (i, settings) {
|
||||
var func = function (data) {
|
||||
if (originalData.errorThrown) {
|
||||
return $.Deferred()
|
||||
.rejectWith(that, [originalData]).promise();
|
||||
}
|
||||
return that.processActions[settings.action].call(
|
||||
that,
|
||||
data,
|
||||
settings
|
||||
);
|
||||
};
|
||||
chain = chain.pipe(func, settings.always && func);
|
||||
});
|
||||
chain
|
||||
.done(function () {
|
||||
that._trigger('processdone', null, data);
|
||||
that._trigger('processalways', null, data);
|
||||
})
|
||||
.fail(function () {
|
||||
that._trigger('processfail', null, data);
|
||||
that._trigger('processalways', null, data);
|
||||
});
|
||||
return chain;
|
||||
},
|
||||
|
||||
// Replaces the settings of each processQueue item that
|
||||
// are strings starting with an "@", using the remaining
|
||||
// substring as key for the option map,
|
||||
// e.g. "@autoUpload" is replaced with options.autoUpload:
|
||||
_transformProcessQueue: function (options) {
|
||||
var processQueue = [];
|
||||
$.each(options.processQueue, function () {
|
||||
var settings = {},
|
||||
action = this.action,
|
||||
prefix = this.prefix === true ? action : this.prefix;
|
||||
$.each(this, function (key, value) {
|
||||
if ($.type(value) === 'string' &&
|
||||
value.charAt(0) === '@') {
|
||||
settings[key] = options[
|
||||
value.slice(1) || (prefix ? prefix +
|
||||
key.charAt(0).toUpperCase() + key.slice(1) : key)
|
||||
];
|
||||
} else {
|
||||
settings[key] = value;
|
||||
}
|
||||
|
||||
});
|
||||
processQueue.push(settings);
|
||||
});
|
||||
options.processQueue = processQueue;
|
||||
},
|
||||
|
||||
// Returns the number of files currently in the processsing queue:
|
||||
processing: function () {
|
||||
return this._processing;
|
||||
},
|
||||
|
||||
// Processes the files given as files property of the data parameter,
|
||||
// returns a Promise object that allows to bind callbacks:
|
||||
process: function (data) {
|
||||
var that = this,
|
||||
options = $.extend({}, this.options, data);
|
||||
if (options.processQueue && options.processQueue.length) {
|
||||
this._transformProcessQueue(options);
|
||||
if (this._processing === 0) {
|
||||
this._trigger('processstart');
|
||||
}
|
||||
$.each(data.files, function (index) {
|
||||
var opts = index ? $.extend({}, options) : options,
|
||||
func = function () {
|
||||
if (data.errorThrown) {
|
||||
return $.Deferred()
|
||||
.rejectWith(that, [data]).promise();
|
||||
}
|
||||
return that._processFile(opts, data);
|
||||
};
|
||||
opts.index = index;
|
||||
that._processing += 1;
|
||||
that._processingQueue = that._processingQueue.pipe(func, func)
|
||||
.always(function () {
|
||||
that._processing -= 1;
|
||||
if (that._processing === 0) {
|
||||
that._trigger('processstop');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return this._processingQueue;
|
||||
},
|
||||
|
||||
_create: function () {
|
||||
this._super();
|
||||
this._processing = 0;
|
||||
this._processingQueue = $.Deferred().resolveWith(this)
|
||||
.promise();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}));
|
||||
1426
wagtail/wagtaildocs/static_src/wagtaildocs/js/vendor/jquery.fileupload.js
vendored
Normal file
1426
wagtail/wagtaildocs/static_src/wagtaildocs/js/vendor/jquery.fileupload.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
214
wagtail/wagtaildocs/static_src/wagtaildocs/js/vendor/jquery.iframe-transport.js
vendored
Normal file
214
wagtail/wagtaildocs/static_src/wagtaildocs/js/vendor/jquery.iframe-transport.js
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}));
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
@import 'wagtailadmin/scss/variables';
|
||||
@import 'wagtailadmin/scss/mixins';
|
||||
@import 'wagtailadmin/scss/grid';
|
||||
|
||||
.replace-file-input {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-bottom: 2px;
|
||||
|
||||
input[type=file] {
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
direction: ltr;
|
||||
width: auto;
|
||||
display: block;
|
||||
font-size: 5em;
|
||||
|
||||
&: hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&: hover {
|
||||
cursor: pointer;
|
||||
|
||||
button {
|
||||
background-color: $color-teal-darker;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-list {
|
||||
> li {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.left {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 150px;
|
||||
display: block;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.progress {
|
||||
@include box-shadow(0 0 5px 2px rgba(255, 255, 255, 0.4));
|
||||
|
||||
max-width: 100%;
|
||||
z-index: 4;
|
||||
margin-left: 20%;
|
||||
margin-right: 20%;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.status-msg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-complete {
|
||||
.progress {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-success {
|
||||
.status-msg.success {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-failure {
|
||||
border-color: $color-red;
|
||||
|
||||
.preview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-msg.failure {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
{% if user_can_add %}
|
||||
{% trans "Add a document" as add_doc_str %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=doc_str add_link="wagtaildocs:add" icon="doc-full-inverse" add_text=add_doc_str search_url="wagtaildocs:index" %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=doc_str add_link="wagtaildocs:add_multiple" icon="doc-full-inverse" add_text=add_doc_str search_url="wagtaildocs:index" %}
|
||||
{% else %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=doc_str icon="doc-full-inverse" search_url="wagtaildocs:index" %}
|
||||
{% endif %}
|
||||
|
|
|
|||
74
wagtail/wagtaildocs/templates/wagtaildocs/multiple/add.html
Normal file
74
wagtail/wagtaildocs/templates/wagtaildocs/multiple/add.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
{% extends "wagtailadmin/base.html" %}
|
||||
{% load i18n static %}
|
||||
{% block titletag %}{% trans "Add multiple documents" %}{% endblock %}
|
||||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
|
||||
<link rel="stylesheet" href="{% static 'wagtaildocs/css/add-multiple.css' %}" type="text/css" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "Add documents" as add_str %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=add_str icon="doc" %}
|
||||
|
||||
<div class="nice-padding">
|
||||
<div class="drop-zone">
|
||||
<p>{% trans "Drag and drop documents into this area to upload immediately." %}</p>
|
||||
<p>{{ help_text }}
|
||||
|
||||
<form action="{% url 'wagtaildocs:add_multiple' %}" method="POST" enctype="multipart/form-data">
|
||||
<div class="replace-file-input">
|
||||
<button class="bicolor icon icon-plus">{% trans "Or choose from your computer" %}</button>
|
||||
<input id="fileupload" type="file" name="files[]" data-url="{% url 'wagtaildocs:add_multiple' %}" multiple>
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="overall-progress" class="progress progress-secondary">
|
||||
<div class="bar" style="width: 0%;">0%</div>
|
||||
</div>
|
||||
|
||||
<ul id="upload-list" class="upload-list multiple"></ul>
|
||||
</div>
|
||||
|
||||
<script id="upload-list-item" type="text/template">
|
||||
<li class="row">
|
||||
<div class="left col3">
|
||||
<div class="preview">
|
||||
<div class="progress">
|
||||
<div class="bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right col9">
|
||||
<p class="status-msg success">{% trans "Upload successful. Please update this documents with a more appropriate title, if necessary. You may also delete the document completely if the upload wasn't required." %}</p>
|
||||
<p class="status-msg failure">{% trans "Sorry, upload failed." %}</p>
|
||||
<p class="status-msg failure error_messages"></p>
|
||||
</div>
|
||||
</li>
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
|
||||
<!-- this exact order of plugins is vital -->
|
||||
<script src="{% static 'wagtaildocs/js/vendor/jquery.iframe-transport.js' %}"></script>
|
||||
<script src="{% static 'wagtaildocs/js/vendor/jquery.fileupload.js' %}"></script>
|
||||
<script src="{% static 'wagtaildocs/js/vendor/jquery.fileupload-process.js' %}"></script>
|
||||
<script src="{% static 'wagtailadmin/js/vendor/tag-it.js' %}"></script>
|
||||
|
||||
<!-- Main script -->
|
||||
<script src="{% static 'wagtaildocs/js/add-multiple.js' %}"></script>
|
||||
|
||||
{% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %}
|
||||
<script>
|
||||
window.fileupload_opts = {
|
||||
simple_upload_url: "{% url 'wagtaildocs:add' %}"
|
||||
}
|
||||
window.tagit_opts = {
|
||||
autocomplete: {source: "{{ autocomplete_url|addslashes }}"}
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{% load i18n %}
|
||||
|
||||
<form action="{% url 'wagtaildocs:edit_multiple' doc.id %}" method="POST" enctype="multipart/form-data">
|
||||
<ul class="fields">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% else %}
|
||||
{% include "wagtailadmin/shared/field_as_li.html" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li>
|
||||
<input type="submit" value="{% trans 'Update' %}" />
|
||||
<a href="{% url 'wagtaildocs:delete_multiple' doc.id %}" class="delete button button-secondary no">{% trans "Delete" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import unittest
|
||||
import mock
|
||||
from bs4 import BeautifulSoup
|
||||
|
|
@ -11,6 +12,7 @@ from django.contrib.auth import get_user_model
|
|||
from django.contrib.auth.models import Group, Permission
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from django.utils.six import b
|
||||
|
|
@ -258,6 +260,192 @@ class TestDocumentDeleteView(TestCase, WagtailTestUtils):
|
|||
self.assertFalse(models.Document.objects.filter(id=self.document.id).exists())
|
||||
|
||||
|
||||
class TestMultipleDocumentUploader(TestCase, WagtailTestUtils):
|
||||
"""
|
||||
This tests the multiple document upload views located in wagtaildocs/views/multiple.py
|
||||
"""
|
||||
def setUp(self):
|
||||
self.login()
|
||||
|
||||
# Create a document for running tests on
|
||||
self.doc = Document.objects.create(
|
||||
title="Test document",
|
||||
file=ContentFile(b("Simple text document")),
|
||||
)
|
||||
|
||||
def test_add(self):
|
||||
"""
|
||||
This tests that the add view responds correctly on a GET request
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.get(reverse('wagtaildocs:add_multiple'))
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtaildocs/multiple/add.html')
|
||||
|
||||
def test_add_post(self):
|
||||
"""
|
||||
This tests that a POST request to the add view saves the document and returns an edit form
|
||||
"""
|
||||
response = self.client.post(reverse('wagtaildocs:add_multiple'), {
|
||||
'files[]': SimpleUploadedFile('test.png', b"Simple text document"),
|
||||
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/json')
|
||||
self.assertTemplateUsed(response, 'wagtaildocs/multiple/edit_form.html')
|
||||
|
||||
# Check document
|
||||
self.assertIn('doc', response.context)
|
||||
self.assertEqual(response.context['doc'].title, 'test.png')
|
||||
self.assertTrue(response.context['doc'].file_size)
|
||||
|
||||
# Check form
|
||||
self.assertIn('form', response.context)
|
||||
self.assertEqual(response.context['form'].initial['title'], 'test.png')
|
||||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn('doc_id', response_json)
|
||||
self.assertIn('form', response_json)
|
||||
self.assertIn('success', response_json)
|
||||
self.assertEqual(response_json['doc_id'], response.context['doc'].id)
|
||||
self.assertTrue(response_json['success'])
|
||||
|
||||
def test_add_post_noajax(self):
|
||||
"""
|
||||
This tests that only AJAX requests are allowed to POST to the add view
|
||||
"""
|
||||
response = self.client.post(reverse('wagtaildocs:add_multiple'), {})
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_add_post_nofile(self):
|
||||
"""
|
||||
This tests that the add view checks for a file when a user POSTs to it
|
||||
"""
|
||||
response = self.client.post(reverse('wagtaildocs:add_multiple'), {}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_edit_get(self):
|
||||
"""
|
||||
This tests that a GET request to the edit view returns a 405 "METHOD NOT ALLOWED" response
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.get(reverse('wagtaildocs:edit_multiple', args=(self.doc.id, )))
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
def test_edit_post(self):
|
||||
"""
|
||||
This tests that a POST request to the edit view edits the document
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.post(reverse('wagtaildocs:edit_multiple', args=(self.doc.id, )), {
|
||||
('doc-%d-title' % self.doc.id): "New title!",
|
||||
('doc-%d-tags' % self.doc.id): "",
|
||||
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/json')
|
||||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn('doc_id', response_json)
|
||||
self.assertNotIn('form', response_json)
|
||||
self.assertIn('success', response_json)
|
||||
self.assertEqual(response_json['doc_id'], self.doc.id)
|
||||
self.assertTrue(response_json['success'])
|
||||
|
||||
def test_edit_post_noajax(self):
|
||||
"""
|
||||
This tests that a POST request to the edit view without AJAX returns a 400 response
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.post(reverse('wagtaildocs:edit_multiple', args=(self.doc.id, )), {
|
||||
('doc-%d-title' % self.doc.id): "New title!",
|
||||
('doc-%d-tags' % self.doc.id): "",
|
||||
})
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_edit_post_validation_error(self):
|
||||
"""
|
||||
This tests that a POST request to the edit page returns a json document with "success=False"
|
||||
and a form with the validation error indicated
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.post(reverse('wagtaildocs:edit_multiple', args=(self.doc.id, )), {
|
||||
('doc-%d-title' % self.doc.id): "", # Required
|
||||
('doc-%d-tags' % self.doc.id): "",
|
||||
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/json')
|
||||
self.assertTemplateUsed(response, 'wagtaildocs/multiple/edit_form.html')
|
||||
|
||||
# Check that a form error was raised
|
||||
self.assertFormError(response, 'form', 'title', "This field is required.")
|
||||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn('doc_id', response_json)
|
||||
self.assertIn('form', response_json)
|
||||
self.assertIn('success', response_json)
|
||||
self.assertEqual(response_json['doc_id'], self.doc.id)
|
||||
self.assertFalse(response_json['success'])
|
||||
|
||||
def test_delete_get(self):
|
||||
"""
|
||||
This tests that a GET request to the delete view returns a 405 "METHOD NOT ALLOWED" response
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.get(reverse('wagtaildocs:delete_multiple', args=(self.doc.id, )))
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
def test_delete_post(self):
|
||||
"""
|
||||
This tests that a POST request to the delete view deletes the document
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.post(reverse('wagtaildocs:delete_multiple', args=(self.doc.id, )), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/json')
|
||||
|
||||
# Make sure the document is deleted
|
||||
self.assertFalse(Document.objects.filter(id=self.doc.id).exists())
|
||||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn('doc_id', response_json)
|
||||
self.assertIn('success', response_json)
|
||||
self.assertEqual(response_json['doc_id'], self.doc.id)
|
||||
self.assertTrue(response_json['success'])
|
||||
|
||||
def test_delete_post_noajax(self):
|
||||
"""
|
||||
This tests that a POST request to the delete view without AJAX returns a 400 response
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.post(reverse('wagtaildocs:delete_multiple', args=(self.doc.id, )))
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
|
||||
class TestDocumentChooserView(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
|
|
|
|||
121
wagtail/wagtaildocs/views/multiple.py
Normal file
121
wagtail/wagtaildocs/views/multiple.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.utils.encoding import force_text
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
from wagtail.utils.compat import render_to_string
|
||||
from wagtail.wagtailadmin.utils import permission_required
|
||||
from wagtail.wagtailsearch.backends import get_search_backends
|
||||
|
||||
from ..models import get_document_model
|
||||
from ..forms import get_document_form, get_document_multi_form
|
||||
|
||||
|
||||
@permission_required('wagtaildocs.add_document')
|
||||
@vary_on_headers('X-Requested-With')
|
||||
def add(request):
|
||||
Document = get_document_model()
|
||||
DocumentForm = get_document_form(Document)
|
||||
DocumentMultiForm = get_document_multi_form(Document)
|
||||
|
||||
if request.method == 'POST':
|
||||
if not request.is_ajax():
|
||||
return HttpResponseBadRequest("Cannot POST to this view without AJAX")
|
||||
|
||||
if not request.FILES:
|
||||
return HttpResponseBadRequest("Must upload a file")
|
||||
|
||||
# Build a form for validation
|
||||
form = DocumentForm(
|
||||
{'title': request.FILES['files[]'].name},
|
||||
{'file': request.FILES['files[]']})
|
||||
|
||||
if form.is_valid():
|
||||
# Save it
|
||||
doc = form.save(commit=False)
|
||||
doc.uploaded_by_user = request.user
|
||||
doc.file_size = doc.file.size
|
||||
doc.save()
|
||||
|
||||
# Success! Send back an edit form for this document to the user
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'doc_id': int(doc.id),
|
||||
'form': render_to_string('wagtaildocs/multiple/edit_form.html', {
|
||||
'doc': doc,
|
||||
'form': DocumentMultiForm(instance=doc, prefix='doc-%d' % doc.id),
|
||||
}, request=request),
|
||||
})
|
||||
else:
|
||||
# Validation error
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
|
||||
# https://github.com/django/django/blob/stable/1.6.x/django/forms/util.py#L45
|
||||
'error_message': '\n'.join(['\n'.join([force_text(i) for i in v]) for k, v in form.errors.items()]),
|
||||
})
|
||||
else:
|
||||
form = DocumentForm()
|
||||
|
||||
return render(request, 'wagtaildocs/multiple/add.html', {
|
||||
'help_text': form.fields['file'].help_text,
|
||||
})
|
||||
|
||||
|
||||
@require_POST
|
||||
def edit(request, doc_id, callback=None):
|
||||
Document = get_document_model()
|
||||
DocumentMultiForm = get_document_multi_form(Document)
|
||||
|
||||
doc = get_object_or_404(Document, id=doc_id)
|
||||
|
||||
if not request.is_ajax():
|
||||
return HttpResponseBadRequest("Cannot POST to this view without AJAX")
|
||||
|
||||
if not doc.is_editable_by_user(request.user):
|
||||
raise PermissionDenied
|
||||
|
||||
form = DocumentMultiForm(request.POST, request.FILES, instance=doc, prefix='doc-' + doc_id)
|
||||
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
# Reindex the doc to make sure all tags are indexed
|
||||
for backend in get_search_backends():
|
||||
backend.add(doc)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'doc_id': int(doc_id),
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'doc_id': int(doc_id),
|
||||
'form': render_to_string('wagtaildocs/multiple/edit_form.html', {
|
||||
'doc': doc,
|
||||
'form': form,
|
||||
}, request=request),
|
||||
})
|
||||
|
||||
|
||||
@require_POST
|
||||
def delete(request, doc_id):
|
||||
Document = get_document_model()
|
||||
|
||||
doc = get_object_or_404(Document, id=doc_id)
|
||||
|
||||
if not request.is_ajax():
|
||||
return HttpResponseBadRequest("Cannot POST to this view without AJAX")
|
||||
|
||||
if not doc.is_editable_by_user(request.user):
|
||||
raise PermissionDenied
|
||||
|
||||
doc.delete()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'doc_id': int(doc_id),
|
||||
})
|
||||
Loading…
Reference in a new issue