Bulk deletion of form submissions (#3233)

Remove actions column

Basic implementation of bulk deletion of form submissions

Moved select all checkbox

Pluralised messages

Hide delete button when it's not needed

Test updates for multiple form submission delete

Use checkbox value instead of submissionId data attribute

Simplify way submission IDs are sent to the server

As per https://github.com/wagtail/wagtail/pull/3233#discussion_r94417831

Removed inline styling

Simplify state management logic

As per:
https://github.com/wagtail/wagtail/pull/3233#discussion_r94426467

Call updateActions on load to bring checkboxes and delete button in sync

Release note for #3233
pull/2775/merge
Karl Hobley 2016-12-19 15:53:44 +00:00 zatwierdzone przez Matt Westcott
rodzic b01376ad72
commit 4f1d3509ff
8 zmienionych plików z 157 dodań i 46 usunięć

Wyświetl plik

@ -4,6 +4,7 @@ Changelog
1.9 (xx.xx.xxxx) - IN DEVELOPMENT
~~~~~~~~~~~~~~~~
* Form builder form submissions can now be bulk-deleted (Karl Hobley)
* `get_context` methods on StreamField blocks can now access variables from the parent context (Mikael Svensson, Peter Baumgartner)
* Added `before_copy_page` and `after_copy_page` hooks (Matheus Bratfisch)
* View live / draft links in the admin now consistently open in a new window (Marco Fucci)

Wyświetl plik

@ -10,6 +10,12 @@ Wagtail 1.9 release notes - IN DEVELOPMENT
What's new
==========
Bulk-deletion of form submissions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Form builder form submissions can now be deleted in bulk from the form submissions index page. This feature was sponsored by St John's College, Oxford and developed by Karl Hobley.
Accessing parent context from StreamField block ``get_context`` methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Wyświetl plik

@ -1,19 +1,23 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% blocktrans with title=page.title %}Delete {{ title }}{% endblocktrans %}{% endblock %}
{% block titletag %}{% blocktrans with title=page.title %}Delete form data {{ title }}{% endblocktrans %}{% endblock %}
{% block bodyclass %}menu-explorer{% endblock %}
{% block content %}
{% trans "Delete" as del_str %}
{% trans "Delete form data" as del_str %}
{% include "wagtailadmin/shared/header.html" with title=del_str subtitle=page.title icon="doc-empty-inverse" %}
<div class="nice-padding">
<p>
{% trans 'Are you sure you want to delete this form submission?' %}
{% blocktrans count counter=submissions.count %}
Are you sure you want to delete this form submission?
{% plural %}
Are you sure you want to delete these form submissions?
{% endblocktrans %}
</p>
<form action="{% url 'wagtailforms:delete_submission' page.id submission.id %}" method="POST">
<form action="{% url 'wagtailforms:delete_submissions' page.id %}?{{ request.GET.urlencode }}" method="POST">
{% csrf_token %}
<input type="submit" value="{% trans 'Delete it' %}" class="button serious">
<input type="submit" value="{% trans 'Delete' %}" class="button serious">
</form>
</div>
{% endblock %}

Wyświetl plik

@ -23,6 +23,63 @@
},
lang: 'lang'
});
var selectAllCheckbox = document.getElementById('select-all');
var deleteButton = document.getElementById('delete-submissions');
var selectedSubmissions = {};
function updateActions() {
var submissionCheckboxes = $('input[type=checkbox].select-submission');
var someSubmissionsSelected = submissionCheckboxes.is(':checked');
var everySubmissionSelected = !submissionCheckboxes.is(':not(:checked)');
// Select all box state
if (everySubmissionSelected) {
// Every submission has been selected
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else if (someSubmissionsSelected) {
// At least one, but not all submissions have been selected
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
} else {
// No submissions have been selected
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
// Delete button state
if (someSubmissionsSelected) {
deleteButton.classList.remove('disabled')
deleteButton.style.visibility = "visible";
} else {
deleteButton.classList.add('disabled')
deleteButton.style.visibility = "hidden";
}
}
// Event handlers
$(selectAllCheckbox).on('change', function() {
let checked = this.checked;
// Update checkbox states
$('input[type=checkbox].select-submission').each(function() {
this.checked = checked;
});
updateActions();
});
$('input[type=checkbox].select-submission').on('change', function() {
updateActions();
});
// initial call to updateActions to bring delete button state in sync with checkboxes
// in the case that some checkboxes are pre-checked (which will be the case in some
// browsers when using the back button)
updateActions();
});
</script>
{% endblock %}
@ -55,10 +112,12 @@
</header>
<div class="nice-padding">
{% if submissions %}
{% include "wagtailforms/list_submissions.html" %}
<form action="{% url 'wagtailforms:delete_submissions' form_page.id %}" method="get">
{% include "wagtailforms/list_submissions.html" %}
{% include "wagtailadmin/shared/pagination_nav.html" with items=submissions is_searching=False linkurl='-' %}
{# Here we pass an invalid non-empty URL name as linkurl to generate pagination links with the URL path omitted #}
{% include "wagtailadmin/shared/pagination_nav.html" with items=submissions is_searching=False linkurl='-' %}
{# Here we pass an invalid non-empty URL name as linkurl to generate pagination links with the URL path omitted #}
</form>
{% else %}
<p class="no-results-message">{% blocktrans with title=form_page.title %}There have been no submissions of the '{{ title }}' form.{% endblocktrans %}</p>
{% endif %}

Wyświetl plik

@ -6,25 +6,29 @@
<col />
<thead>
<tr>
<th colspan="{{ data_headings|length|add:1 }}">
<button class="button no" id="delete-submissions" style="visibility: hidden">Delete selected submissions</button>
</th>
</tr>
<tr>
<th><input type="checkbox" id="select-all" /></th>
{% for heading in data_headings %}
<th>{{ heading }}</th>
{% endfor %}
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for row in data_rows %}
<tr>
<td>
<input type="checkbox" name="selected-submissions" class="select-submission" value="{{ row.model_id }}" />
</td>
{% for cell in row.fields %}
<td>
{{ cell }}
</td>
{% endfor %}
<td>
<a class="button button-small button-secondary" href="
{% url 'wagtailforms:delete_submission' form_page.id row.model_id %}">
{% trans 'delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>

Wyświetl plik

@ -767,11 +767,11 @@ class TestDeleteFormSubmission(TestCase, WagtailTestUtils):
self.assertTrue(self.client.login(username='siteeditor', password='password'))
self.form_page = Page.objects.get(url_path='/home/contact-us/')
def test_delete_submission_show_cofirmation(self):
def test_delete_submission_show_confirmation(self):
response = self.client.get(reverse(
'wagtailforms:delete_submission',
args=(self.form_page.id, FormSubmission.objects.first().id)
))
'wagtailforms:delete_submissions',
args=(self.form_page.id, )
) + '?selected-submissions={}'.format(FormSubmission.objects.first().id))
# Check show confirm page when HTTP method is GET
self.assertTemplateUsed(response, 'wagtailforms/confirm_delete.html')
@ -780,22 +780,33 @@ class TestDeleteFormSubmission(TestCase, WagtailTestUtils):
def test_delete_submission_with_permissions(self):
response = self.client.post(reverse(
'wagtailforms:delete_submission',
args=(self.form_page.id, FormSubmission.objects.first().id)
))
'wagtailforms:delete_submissions',
args=(self.form_page.id, )
) + '?selected-submissions={}'.format(FormSubmission.objects.first().id))
# Check that the submission is gone
self.assertEqual(FormSubmission.objects.count(), 1)
# Should be redirected to list of submissions
self.assertRedirects(response, reverse("wagtailforms:list_submissions", args=(self.form_page.id,)))
def test_delete_multiple_submissions_with_permissions(self):
response = self.client.post(reverse(
'wagtailforms:delete_submissions',
args=(self.form_page.id, )
) + '?selected-submissions={}&selected-submissions={}'.format(FormSubmission.objects.first().id, FormSubmission.objects.last().id))
# Check that both submissions are gone
self.assertEqual(FormSubmission.objects.count(), 0)
# Should be redirected to list of submissions
self.assertRedirects(response, reverse("wagtailforms:list_submissions", args=(self.form_page.id,)))
def test_delete_submission_bad_permissions(self):
self.assertTrue(self.client.login(username="eventeditor", password="password"))
response = self.client.post(reverse(
'wagtailforms:delete_submission',
args=(self.form_page.id, FormSubmission.objects.first().id)
))
'wagtailforms:delete_submissions',
args=(self.form_page.id, )
) + '?selected-submissions={}'.format(FormSubmission.objects.first().id))
# Check that the user received a 403 response
self.assertEqual(response.status_code, 403)
@ -810,9 +821,9 @@ class TestDeleteFormSubmission(TestCase, WagtailTestUtils):
with self.register_hook('filter_form_submissions_for_user', construct_forms_for_user):
response = self.client.post(reverse(
'wagtailforms:delete_submission',
args=(self.form_page.id, FormSubmission.objects.first().id)
))
'wagtailforms:delete_submissions',
args=(self.form_page.id, )
) + '?selected-submissions={}'.format(FormSubmission.objects.first().id))
# An user can't delete a from submission with the hook
self.assertEqual(response.status_code, 403)
@ -820,9 +831,9 @@ class TestDeleteFormSubmission(TestCase, WagtailTestUtils):
# An user can delete a form submission without the hook
response = self.client.post(reverse(
'wagtailforms:delete_submission',
args=(self.form_page.id, FormSubmission.objects.first().id)
))
'wagtailforms:delete_submissions',
args=(self.form_page.id, )
) + '?selected-submissions={}'.format(CustomFormPageSubmission.objects.first().id))
self.assertEqual(FormSubmission.objects.count(), 1)
self.assertRedirects(response, reverse("wagtailforms:list_submissions", args=(self.form_page.id,)))
@ -834,11 +845,12 @@ class TestDeleteCustomFormSubmission(TestCase):
self.assertTrue(self.client.login(username='siteeditor', password='password'))
self.form_page = Page.objects.get(url_path='/home/contact-us-one-more-time/')
def test_delete_submission_show_cofirmation(self):
def test_delete_submission_show_confirmation(self):
response = self.client.get(reverse(
'wagtailforms:delete_submission',
args=(self.form_page.id, CustomFormPageSubmission.objects.first().id)
))
'wagtailforms:delete_submissions',
args=(self.form_page.id, )
) + '?selected-submissions={}'.format(CustomFormPageSubmission.objects.first().id))
# Check show confirm page when HTTP method is GET
self.assertTemplateUsed(response, 'wagtailforms/confirm_delete.html')
@ -847,22 +859,33 @@ class TestDeleteCustomFormSubmission(TestCase):
def test_delete_submission_with_permissions(self):
response = self.client.post(reverse(
'wagtailforms:delete_submission',
args=(self.form_page.id, CustomFormPageSubmission.objects.first().id)
))
'wagtailforms:delete_submissions',
args=(self.form_page.id, )
) + '?selected-submissions={}'.format(CustomFormPageSubmission.objects.first().id))
# Check that the submission is gone
self.assertEqual(CustomFormPageSubmission.objects.count(), 1)
# Should be redirected to list of submissions
self.assertRedirects(response, reverse("wagtailforms:list_submissions", args=(self.form_page.id, )))
def test_delete_multiple_submissions_with_permissions(self):
response = self.client.post(reverse(
'wagtailforms:delete_submissions',
args=(self.form_page.id, )
) + '?selected-submissions={}&selected-submissions={}'.format(CustomFormPageSubmission.objects.first().id, CustomFormPageSubmission.objects.last().id))
# Check that both submissions are gone
self.assertEqual(CustomFormPageSubmission.objects.count(), 0)
# Should be redirected to list of submissions
self.assertRedirects(response, reverse("wagtailforms:list_submissions", args=(self.form_page.id,)))
def test_delete_submission_bad_permissions(self):
self.assertTrue(self.client.login(username="eventeditor", password="password"))
response = self.client.post(reverse(
'wagtailforms:delete_submission',
args=(self.form_page.id, CustomFormPageSubmission.objects.first().id)
))
'wagtailforms:delete_submissions',
args=(self.form_page.id, )
) + '?selected-submissions={}'.format(CustomFormPageSubmission.objects.first().id))
# Check that the user received a 403 response
self.assertEqual(response.status_code, 403)

Wyświetl plik

@ -7,5 +7,5 @@ from wagtail.wagtailforms import views
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^submissions/(\d+)/$', views.list_submissions, name='list_submissions'),
url(r'^submissions/(\d+)/(\d+)/delete/$', views.delete_submission, name='delete_submission')
url(r'^submissions/(\d+)/delete/$', views.delete_submissions, name='delete_submissions')
]

Wyświetl plik

@ -7,7 +7,7 @@ from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.encoding import smart_str
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
from wagtail.utils.pagination import paginate
from wagtail.wagtailadmin import messages
@ -26,22 +26,36 @@ def index(request):
})
def delete_submission(request, page_id, submission_id):
def delete_submissions(request, page_id):
if not get_forms_for_user(request.user).filter(id=page_id).exists():
raise PermissionDenied
page = get_object_or_404(Page, id=page_id).specific
submission = get_object_or_404(page.get_submission_class(), id=submission_id)
# Get submissions
submission_ids = request.GET.getlist('selected-submissions')
submissions = page.get_submission_class()._default_manager.filter(id__in=submission_ids)
if request.method == 'POST':
submission.delete()
count = submissions.count()
submissions.delete()
messages.success(
request,
ungettext(
"One submission has been deleted.",
"%(count)d submissions have been deleted.",
count
) % {
'count': count,
}
)
messages.success(request, _("Submission deleted."))
return redirect('wagtailforms:list_submissions', page_id)
return render(request, 'wagtailforms/confirm_delete.html', {
'page': page,
'submission': submission
'submissions': submissions,
})