Upload multiple documents at once

Functionality from the multiple image uploader was used.
This commit is contained in:
Tim Heap 2015-11-02 15:29:23 +11:00 committed by Matt Westcott
parent ff9289fdb0
commit f0095b5882
12 changed files with 2467 additions and 2 deletions

View file

@ -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'),

View file

@ -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()
})

View 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()});
}
});
});
});

View 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();
}
});
}));

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,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;
}
}
}

View file

@ -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 %}

View 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 %}

View file

@ -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>

View file

@ -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()

View 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),
})