0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-25 05:02:57 +01:00

Use class based views instead of function views

Changes `index`, `delete_submissions` and `list_submissions`.
Adjusted view tests to properly test the new class based structure.
remove get_field_ordering from FormPage model - now on view
This commit is contained in:
Johan Arensman 2017-09-18 21:17:12 +02:00 committed by LB
parent 161b47d436
commit 815cb6e405
15 changed files with 665 additions and 196 deletions

View File

@ -29,6 +29,7 @@ Changelog
* Upgraded jQuery to version 3.2.1 (Janneke Janssen)
* Updated documentation styling (LB (Ben Johnston))
* Rich text fields now take feature lists into account when whitelisting HTML elements (Matt Westcott)
* FormPage lists and Form submission lists in admin now use class based views for easy overriding (Johan Arensman)
* Fix: Do not remove stopwords when generating slugs from non-ASCII titles, to avoid issues with incorrect word boundaries (Sævar Öfjörð Magnússon)
* Fix: The PostgreSQL search backend now preserves ordering of the `QuerySet` when searching with `order_by_relevance=False` (Bertrand Bordage)
* Fix: Using `modeladmin_register` as a decorator no longer replaces the decorated class with `None` (Tim Heap)

View File

@ -604,3 +604,46 @@ Finally, we add a URL param of `id` based on the ``form_submission`` if it exist
FieldPanel('subject'),
], 'Email'),
]
Customise form submissions listing in Wagtail Admin
---------------------------------------------------
The Admin listing of form submissions can be customised by setting the attribute ``submissions_list_view_class`` on your FormPage model.
The list view class must be a subclass of ``SubmissionsListView`` from ``wagtail.contrib.forms.views``, which is a child class of `Django's class based ListView <https://docs.djangoproject.com/en/2.0/ref/class-based-views/generic-display/#listview>`_.
Example:
.. code-block:: python
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
from wagtail.contrib.forms.views import SubmissionsListView
class CustomSubmissionsListView(SubmissionsListView):
paginate_by = 50 # show more submissions per page, default is 20
ordering = ('submit_time',) # order submissions by oldest first, normally newest first
ordering_csv = ('-submit_time',) # order csv export by newest first, normally oldest first
# override the method to generate csv filename
def get_csv_filename(self):
""" Returns the filename for CSV file with page title at start"""
filename = super().get_csv_filename()
return self.form_page.slug + '-' + filename
class FormField(AbstractFormField):
page = ParentalKey('FormPage', related_name='form_fields')
class FormPage(AbstractEmailForm):
"""Form Page with customised submissions listing view"""
# set custom view class as class attribute
submissions_list_view_class = CustomSubmissionsListView
intro = RichTextField(blank=True)
thank_you_text = RichTextField(blank=True)
# content_panels = ...

View File

@ -42,6 +42,7 @@ Other features
* Upgraded jQuery to version 3.2.1 (Janneke Janssen)
* Updated documentation styling (LB (Ben Johnston))
* Rich text fields now take feature lists into account when whitelisting HTML elements (Matt Westcott)
* FormPage lists and Form submission lists in admin now use class based views for easy overriding (Johan Arensman)
Bug fixes
~~~~~~~~~

View File

@ -1,7 +1,6 @@
import json
import os
from django.contrib.contenttypes.models import ContentType
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.shortcuts import render
@ -11,10 +10,12 @@ from unidecode import unidecode
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.admin.utils import send_mail
from wagtail.core import hooks
from wagtail.core.models import Orderable, Page, UserPagePermissionsProxy, get_page_models
from wagtail.core.models import Orderable, Page
from .forms import FormBuilder, WagtailAdminFormPageForm
from .views import SubmissionsListView
FORM_FIELD_CHOICES = (
('singleline', _('Single line text')),
@ -117,45 +118,16 @@ class AbstractFormField(Orderable):
ordering = ['sort_order']
_FORM_CONTENT_TYPES = None
def get_form_types():
global _FORM_CONTENT_TYPES
if _FORM_CONTENT_TYPES is None:
form_models = [
model for model in get_page_models()
if issubclass(model, AbstractForm)
]
_FORM_CONTENT_TYPES = list(
ContentType.objects.get_for_models(*form_models).values()
)
return _FORM_CONTENT_TYPES
def get_forms_for_user(user):
"""
Return a queryset of form pages that this user is allowed to access the submissions for
"""
editable_forms = UserPagePermissionsProxy(user).editable_pages()
editable_forms = editable_forms.filter(content_type__in=get_form_types())
# Apply hooks
for fn in hooks.get_hooks('filter_form_submissions_for_user'):
editable_forms = fn(user, editable_forms)
return editable_forms
class AbstractForm(Page):
"""
A Form Page. Pages implementing a form should inherit from it
"""
base_form_class = WagtailAdminFormPageForm
form_builder = FormBuilder
base_form_class = WagtailAdminFormPageForm
submissions_list_view_class = SubmissionsListView
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -207,33 +179,6 @@ class AbstractForm(Page):
def get_landing_page_template(self, request, *args, **kwargs):
return self.landing_page_template
def get_field_ordering(self, ordering_list, default=('submit_time', 'descending')):
"""
Accepts a list of strings ['-submit_time', 'id']
Returns a list of tuples [(field_name, 'ascending'/'descending'), ...]
Intented to be used to process ordering via request.GET.getlist('order_by')
Checks if the field options are valid, only returns valid, de-duplicated options
invalid options are simply ignored - no error created
"""
valid_fields = ['id', 'submit_time']
field_ordering = []
if len(ordering_list) == 0:
return [default]
for ordering in ordering_list:
try:
none, prefix, field_name = ordering.rpartition('-')
if field_name not in valid_fields:
continue # Invalid field_name, skip it
# only add to ordering if the field is not already set
if field_name not in [order[0] for order in field_ordering]:
asc_desc = 'ascending'
if prefix == '-':
asc_desc = 'descending'
field_ordering.append((field_name, asc_desc))
except (IndexError, ValueError):
continue # Invalid ordering specified, skip it
return field_ordering
def get_submission_class(self):
"""
Returns submission class.
@ -273,6 +218,16 @@ class AbstractForm(Page):
context
)
def serve_submissions_list_view(self, request, *args, **kwargs):
"""
Returns list submissions view for admin.
`list_submissions_view_class` can bse set to provide custom view class.
Your class must be inherited from SubmissionsListView.
"""
view = self.submissions_list_view_class.as_view()
return view(request, form_page=self, *args, **kwargs)
def serve(self, request, *args, **kwargs):
if request.method == 'POST':
form = self.get_form(request.POST, request.FILES, page=self, user=request.user)

View File

@ -114,8 +114,7 @@
{% if submissions %}
<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 %}
{% include "wagtailadmin/shared/pagination_nav.html" with items=page_obj 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 %}

View File

@ -6,13 +6,13 @@
<col />
<thead>
<tr>
<th colspan="{{ data_fields_with_ordering|length|add:1 }}">
<th colspan="{{ data_headings|length|add:1 }}">
<button class="button no" id="delete-submissions" style="visibility: hidden">{% trans "Delete selected submissions" %}</button>
</th>
</tr>
<tr>
<th><input type="checkbox" id="select-all" /></th>
{% for heading in data_fields_with_ordering %}
{% for heading in data_headings %}
<th id="{{ heading.name }}" class="{% if heading.order %}ordered {{ heading.order }}{% endif %}">
{% if heading.order %}<a href="?order_by={% if heading.order == 'ascending' %}-{% endif %}{{ heading.name }}">{{ heading.label }}</a>{% else %}{{ heading.label }}{% endif %}
</th>

View File

@ -2,7 +2,7 @@
{% if form_pages %}
{% include "wagtailforms/list_forms.html" %}
{% include "wagtailadmin/shared/pagination_nav.html" with items=form_pages linkurl="wagtailforms:index" %}
{% include "wagtailadmin/shared/pagination_nav.html" with items=page_obj linkurl="wagtailforms:index" %}
{% else %}
<p>{% trans "No form pages have been created." %}</p>
{% endif %}

View File

@ -5,20 +5,22 @@ from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from wagtail.tests.testapp.models import (
CustomFormPageSubmission, FormField, FormFieldWithCustomSubmission,
FormPage, FormPageWithCustomSubmission)
from wagtail.tests.utils import WagtailTestUtils
from wagtail.admin.edit_handlers import get_form_for_model
from wagtail.admin.forms import WagtailAdminPageForm
from wagtail.core.models import Page
from wagtail.contrib.forms.edit_handlers import FormSubmissionsPanel
from wagtail.contrib.forms.models import FormSubmission
from wagtail.contrib.forms.tests.utils import make_form_page, make_form_page_with_custom_submission
from wagtail.core.models import Page
from wagtail.tests.testapp.models import (
CustomFormPageSubmission, FormField, FormFieldForCustomListViewPage,
FormFieldWithCustomSubmission, FormPage, FormPageWithCustomSubmission,
FormPageWithCustomSubmissionListView)
from wagtail.tests.utils import WagtailTestUtils
class TestFormResponsesPanel(TestCase):
def setUp(self):
self.form_page = make_form_page()
self.FormPageForm = get_form_for_model(
@ -133,7 +135,7 @@ class TestFormsIndex(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailforms/index.html')
# Check that we got the correct page
self.assertEqual(response.context['form_pages'].number, 2)
self.assertEqual(response.context['page_obj'].number, 2)
def test_forms_index_pagination_invalid(self):
# Create some more form pages to make pagination kick in
@ -147,7 +149,7 @@ class TestFormsIndex(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailforms/index.html')
# Check that it got page one
self.assertEqual(response.context['form_pages'].number, 1)
self.assertEqual(response.context['page_obj'].number, 1)
def test_forms_index_pagination_out_of_range(self):
# Create some more form pages to make pagination kick in
@ -161,7 +163,7 @@ class TestFormsIndex(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailforms/index.html')
# Check that it got the last page
self.assertEqual(response.context['form_pages'].number, response.context['form_pages'].paginator.num_pages)
self.assertEqual(response.context['page_obj'].number, response.context['paginator'].num_pages)
def test_cannot_see_forms_without_permission(self):
# Login with as a user without permission to see forms
@ -315,7 +317,7 @@ class TestFormsSubmissionsList(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html')
# Check that we got the correct page
self.assertEqual(response.context['submissions'].number, 2)
self.assertEqual(response.context['page_obj'].number, 2)
def test_list_submissions_pagination_invalid(self):
self.make_list_submissions()
@ -329,7 +331,7 @@ class TestFormsSubmissionsList(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html')
# Check that we got page one
self.assertEqual(response.context['submissions'].number, 1)
self.assertEqual(response.context['page_obj'].number, 1)
def test_list_submissions_pagination_out_of_range(self):
self.make_list_submissions()
@ -341,7 +343,7 @@ class TestFormsSubmissionsList(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html')
# Check that we got the last page
self.assertEqual(response.context['submissions'].number, response.context['submissions'].paginator.num_pages)
self.assertEqual(response.context['page_obj'].number, response.context['paginator'].num_pages)
def test_list_submissions_default_order(self):
response = self.client.get(reverse(
@ -415,6 +417,28 @@ class TestFormsSubmissionsExport(TestCase, WagtailTestUtils):
self.assertEqual(data_lines[1], '2013-01-01 12:00:00+00:00,old@example.com,this is a really old message,"foo, baz"\r')
self.assertEqual(data_lines[2], '2014-01-01 12:00:00+00:00,new@example.com,this is a fairly new message,None\r')
def test_list_submissions_csv_large_export(self):
for i in range(100):
new_form_submission = FormSubmission.objects.create(
page=self.form_page,
form_data=json.dumps({
'your-email': "new@example-%s.com" % i,
'your-message': "I like things x %s" % i,
}),
)
new_form_submission.submit_time = '2014-01-01T12:00:00.000Z'
new_form_submission.save()
response = self.client.get(
reverse('wagtailforms:list_submissions', args=(self.form_page.id,)),
{'action': 'CSV'}
)
# Check that csv export is not paginated
self.assertEqual(response.status_code, 200)
data_lines = response.content.decode().split("\n")
self.assertEqual(104, len(data_lines))
def test_list_submissions_csv_export_after_filter_form_submissions_for_user_hook(self):
# Hook forbids to delete form submissions for everyone
def construct_forms_for_user(user, queryset):
@ -800,7 +824,7 @@ class TestCustomFormsSubmissionsList(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html')
# Check that we got the correct page
self.assertEqual(response.context['submissions'].number, 2)
self.assertEqual(response.context['page_obj'].number, 2)
# CustomFormPageSubmission have custom field. This field should appear in the listing
self.assertContains(response, '<th id="username" class="">Username</th>', html=True)
@ -818,7 +842,7 @@ class TestCustomFormsSubmissionsList(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html')
# Check that we got page one
self.assertEqual(response.context['submissions'].number, 1)
self.assertEqual(response.context['page_obj'].number, 1)
def test_list_submissions_pagination_out_of_range(self):
self.make_list_submissions()
@ -831,8 +855,7 @@ class TestCustomFormsSubmissionsList(TestCase, WagtailTestUtils):
self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html')
# Check that we got the last page
self.assertEqual(response.context['submissions'].number,
response.context['submissions'].paginator.num_pages)
self.assertEqual(response.context['page_obj'].number, response.context['paginator'].num_pages)
class TestDeleteFormSubmission(TestCase, WagtailTestUtils):
@ -969,6 +992,146 @@ class TestDeleteCustomFormSubmission(TestCase):
self.assertEqual(CustomFormPageSubmission.objects.count(), 2)
class TestFormsWithCustomSubmissionsList(TestCase, WagtailTestUtils):
def create_test_user_without_admin(self, username):
return get_user_model().objects.create_user(username=username, password='123')
def setUp(self):
# Create a form page
home_page = Page.objects.get(url_path='/home/')
self.form_page = home_page.add_child(
instance=FormPageWithCustomSubmissionListView(
title='Willy Wonka Chocolate Ideas',
slug='willy-wonka-chocolate-ideas',
to_address='willy@wonka.com',
from_address='info@wonka.com',
subject='Chocolate Idea Submitted!'
)
)
FormFieldForCustomListViewPage.objects.create(
page=self.form_page, sort_order=1, label='Your email', field_type='email', required=True,
)
FormFieldForCustomListViewPage.objects.create(
page=self.form_page, sort_order=2, label='Chocolate', field_type='singleline', required=True,
)
FormFieldForCustomListViewPage.objects.create(
page=self.form_page, sort_order=3, label='Ingredients', field_type='multiline', required=True,
)
self.choices = ['What is chocolate?', 'Mediocre', 'Much excitement', 'Wet my pants excited!']
FormFieldForCustomListViewPage.objects.create(
page=self.form_page, sort_order=4, label='Your Excitement', field_type='radio', required=True,
choices=','.join(self.choices),
)
self.test_user_1 = self.create_test_user_without_admin('user-chocolate-maniac')
self.test_user_2 = self.create_test_user_without_admin('user-chocolate-guy')
# add a couple of initial form submissions for testing ordering
new_form_submission = CustomFormPageSubmission.objects.create(
page=self.form_page,
user=self.test_user_1,
form_data=json.dumps({
'your-email': 'new@example.com',
'chocolate': 'White Chocolate',
'ingredients': 'White colouring',
'your-excitement': self.choices[2],
}),
)
new_form_submission.submit_time = '2017-10-01T12:00:00.000Z'
new_form_submission.save()
old_form_submission = CustomFormPageSubmission.objects.create(
page=self.form_page,
user=self.test_user_2,
form_data=json.dumps({
'your-email': 'old@example.com',
'chocolate': 'Dark Chocolate',
'ingredients': 'Charcoal',
'your-excitement': self.choices[0],
}),
)
old_form_submission.submit_time = '2017-01-01T12:00:00.000Z'
old_form_submission.save()
self.login()
def make_list_submissions(self):
""" Make 100 submissions to test pagination on the forms submissions page """
for i in range(120):
submission = CustomFormPageSubmission(
page=self.form_page,
user=self.test_user_1,
form_data=json.dumps({
'your-email': "foo-%s@bar.com" % i,
'chocolate': 'Chocolate No.%s' % i,
'your-excitement': self.choices[3],
}),
)
submission.save()
def test_list_submissions(self):
response = self.client.get(reverse('wagtailforms:list_submissions', args=(self.form_page.id,)))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html')
self.assertEqual(len(response.context['data_rows']), 2)
# check display of list values within form submissions
self.assertContains(response, 'Much excitement')
self.assertContains(response, 'White Chocolate')
self.assertContains(response, 'Dark Chocolate')
def test_list_submissions_pagination(self):
self.make_list_submissions()
response = self.client.get(reverse('wagtailforms:list_submissions', args=(self.form_page.id,)), {'p': 2})
# Check response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html')
# test that paginate by 50 is working, should be 3 max pages (~120 values)
self.assertContains(response, 'Page 2 of 3')
self.assertContains(response, 'Wet my pants excited!', count=50)
self.assertEqual(response.context['page_obj'].number, 2)
def test_list_submissions_csv_export(self):
response = self.client.get(
reverse('wagtailforms:list_submissions', args=(self.form_page.id,)),
{'action': 'CSV'}
)
# Check response
self.assertEqual(response.status_code, 200)
data_lines = response.content.decode().split("\n")
self.assertIn('filename=%s-export' % self.form_page.slug, response.get('Content-Disposition'))
self.assertEqual(data_lines[0], 'Username,Submission date,Your email,Chocolate,Ingredients,Your Excitement\r')
# first result should be the most recent as order_csv has been reversed
self.assertEqual(data_lines[1], 'user-chocolate-maniac,2017-10-01 12:00:00+00:00,new@example.com,White Chocolate,White colouring,Much excitement\r')
self.assertEqual(data_lines[2], 'user-chocolate-guy,2017-01-01 12:00:00+00:00,old@example.com,Dark Chocolate,Charcoal,What is chocolate?\r')
def test_list_submissions_ordering(self):
form_submission = CustomFormPageSubmission.objects.create(
page=self.form_page,
user=self.create_test_user_without_admin('user-aaa-aaa'),
form_data=json.dumps({
'your-email': 'new@example.com',
'chocolate': 'Old chocolate idea',
'ingredients': 'Sugar',
'your-excitement': self.choices[2],
}),
)
form_submission.submit_time = '2016-01-01T12:00:00.000Z'
form_submission.save()
# check ordering matches default which is overriden to be 'submit_time' (oldest first)
response = self.client.get(reverse('wagtailforms:list_submissions', args=(self.form_page.id,)))
first_row_values = response.context['data_rows'][0]['fields']
self.assertTrue('Old chocolate idea' in first_row_values)
class TestIssue585(TestCase):
fixtures = ['test.json']

View File

@ -1,10 +1,10 @@
from django.conf.urls import url
from wagtail.contrib.forms import views
from wagtail.contrib.forms.views import DeleteSubmissionsView, FormPagesListView, get_submissions_list_view
app_name = 'wagtailforms'
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^submissions/(\d+)/$', views.list_submissions, name='list_submissions'),
url(r'^submissions/(\d+)/delete/$', views.delete_submissions, name='delete_submissions')
url(r'^$', FormPagesListView.as_view(), name='index'),
url(r'^submissions/(?P<page_id>\d+)/$', get_submissions_list_view, name='list_submissions'),
url(r'^submissions/(?P<page_id>\d+)/delete/$', DeleteSubmissionsView.as_view(), name='delete_submissions')
]

View File

@ -0,0 +1,36 @@
from django.contrib.contenttypes.models import ContentType
from wagtail.core import hooks
from wagtail.core.models import UserPagePermissionsProxy, get_page_models
_FORM_CONTENT_TYPES = None
def get_form_types():
global _FORM_CONTENT_TYPES
if _FORM_CONTENT_TYPES is None:
from wagtail.contrib.forms.models import AbstractForm
form_models = [
model for model in get_page_models()
if issubclass(model, AbstractForm)
]
_FORM_CONTENT_TYPES = list(
ContentType.objects.get_for_models(*form_models).values()
)
return _FORM_CONTENT_TYPES
def get_forms_for_user(user):
"""
Return a queryset of form pages that this user is allowed to access the submissions for
"""
editable_forms = UserPagePermissionsProxy(user).editable_pages()
editable_forms = editable_forms.filter(content_type__in=get_form_types())
# Apply hooks
for fn in hooks.get_hooks('filter_form_submissions_for_user'):
editable_forms = fn(user, editable_forms)
return editable_forms

View File

@ -2,152 +2,311 @@ import csv
import datetime
from django.core.exceptions import PermissionDenied
from django.core.paginator import InvalidPage
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, redirect
from django.utils.encoding import smart_str
from django.utils.translation import ungettext
from django.views.generic import ListView, TemplateView
from wagtail.utils.pagination import paginate
from wagtail.admin import messages
from wagtail.core.models import Page
from wagtail.contrib.forms.forms import SelectDateForm
from wagtail.contrib.forms.models import get_forms_for_user
from wagtail.contrib.forms.utils import get_forms_for_user
from wagtail.core.models import Page
from wagtail.utils.pagination import DEFAULT_PAGE_KEY
def index(request):
form_pages = get_forms_for_user(request.user)
paginator, form_pages = paginate(request, form_pages)
return render(request, 'wagtailforms/index.html', {
'form_pages': form_pages,
})
def get_submissions_list_view(request, *args, **kwargs):
""" Call the form page's list submissions view class """
page_id = kwargs.get('page_id')
form_page = get_object_or_404(Page, id=page_id).specific
return form_page.serve_submissions_list_view(request, *args, **kwargs)
def delete_submissions(request, page_id):
if not get_forms_for_user(request.user).filter(id=page_id).exists():
raise PermissionDenied
class SafePaginateListView(ListView):
""" Listing view with safe pagination, allowing incorrect or out of range values """
page = get_object_or_404(Page, id=page_id).specific
paginate_by = 20
page_kwarg = DEFAULT_PAGE_KEY
# Get submissions
submission_ids = request.GET.getlist('selected-submissions')
submissions = page.get_submission_class()._default_manager.filter(id__in=submission_ids)
def paginate_queryset(self, queryset, page_size):
"""Paginate the queryset if needed with nice defaults on invalid param."""
paginator = self.get_paginator(
queryset,
page_size,
orphans=self.get_paginate_orphans(),
allow_empty_first_page=self.get_allow_empty()
)
page_kwarg = self.page_kwarg
page_request = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 0
try:
page_number = int(page_request)
except ValueError:
if page_request == 'last':
page_number = paginator.num_pages
else:
page_number = 0
try:
if page_number > paginator.num_pages:
page_number = paginator.num_pages # page out of range, show last page
page = paginator.page(page_number)
return (paginator, page, page.object_list, page.has_other_pages())
except InvalidPage:
page = paginator.page(1)
return (paginator, page, page.object_list, page.has_other_pages())
return super().paginage_queryset(queryset, page_size)
if request.method == 'POST':
class FormPagesListView(SafePaginateListView):
""" Lists the available form pages for the current user """
template_name = 'wagtailforms/index.html'
context_object_name = 'form_pages'
def get_queryset(self):
""" Return the queryset of form pages for this view """
queryset = get_forms_for_user(self.request.user)
ordering = self.get_ordering()
if ordering:
if isinstance(ordering, str):
ordering = (ordering,)
queryset = queryset.order_by(*ordering)
return queryset
class DeleteSubmissionsView(TemplateView):
""" Delete the selected submissions """
template_name = 'wagtailforms/confirm_delete.html'
page = None
submissions = None
success_url = 'wagtailforms:list_submissions'
def get_queryset(self):
""" Returns a queryset for the selected submissions """
submission_ids = self.request.GET.getlist('selected-submissions')
submission_class = self.page.get_submission_class()
return submission_class._default_manager.filter(id__in=submission_ids)
def handle_delete(self, submissions):
""" Deletes the given queryset """
count = submissions.count()
submissions.delete()
messages.success(
request,
self.request,
ungettext(
"One submission has been deleted.",
"%(count)d submissions have been deleted.",
'One submission has been deleted.',
'%(count)d submissions have been deleted.',
count
) % {
'count': count,
}
) % {'count': count}
)
return redirect('wagtailforms:list_submissions', page_id)
def get_success_url(self):
""" Returns the success URL to redirect to after a successful deletion """
return self.success_url
return render(request, 'wagtailforms/confirm_delete.html', {
'page': page,
'submissions': submissions,
})
def dispatch(self, request, *args, **kwargs):
""" Check permissions, set the page and submissions, handle delete """
page_id = kwargs.get('page_id')
if not get_forms_for_user(self.request.user).filter(id=page_id).exists():
raise PermissionDenied
def list_submissions(request, page_id):
if not get_forms_for_user(request.user).filter(id=page_id).exists():
raise PermissionDenied
self.page = get_object_or_404(Page, id=page_id).specific
form_page = get_object_or_404(Page, id=page_id).specific
form_submission_class = form_page.get_submission_class()
self.submissions = self.get_queryset()
data_fields = form_page.get_data_fields()
if self.request.method == 'POST':
self.handle_delete(self.submissions)
return redirect(self.get_success_url(), page_id)
ordering = form_page.get_field_ordering(request.GET.getlist('order_by'))
return super().dispatch(request, *args, **kwargs)
# convert ordering tuples to a list of strings like ['-submit_time']
ordering_strings = [
'%s%s' % ('-' if order_str[1] == 'descending' else '', order_str[0])
for order_str in ordering]
def get_context_data(self, **kwargs):
""" Get the context for this view """
context = super().get_context_data(**kwargs)
if request.GET.get('action') == 'CSV':
# Revert to CSV being sorted submit_time ascending for backwards compatibility
submissions = form_submission_class.objects.filter(page=form_page).order_by('submit_time')
else:
submissions = form_submission_class.objects.filter(page=form_page).order_by(*ordering_strings)
data_fields_with_ordering = []
for name, label in data_fields:
order = None
for order_value in [o[1] for o in ordering if o[0] == name]:
order = order_value
data_fields_with_ordering.append({
"name": name,
"label": label,
"order": order,
context.update({
'page': self.page,
'submissions': self.submissions,
})
data_headings = [label for name, label in data_fields]
return context
select_date_form = SelectDateForm(request.GET)
if select_date_form.is_valid():
date_from = select_date_form.cleaned_data.get('date_from')
date_to = select_date_form.cleaned_data.get('date_to')
# careful: date_to should be increased by 1 day since the submit_time
# is a time so it will always be greater
if date_to:
date_to += datetime.timedelta(days=1)
if date_from and date_to:
submissions = submissions.filter(submit_time__range=[date_from, date_to])
elif date_from and not date_to:
submissions = submissions.filter(submit_time__gte=date_from)
elif not date_from and date_to:
submissions = submissions.filter(submit_time__lte=date_to)
if request.GET.get('action') == 'CSV':
# return a CSV instead
class SubmissionsListView(SafePaginateListView):
""" Lists submissions for the provided form page """
template_name = 'wagtailforms/index_submissions.html'
context_object_name = 'submissions'
form_page = None
ordering = ('-submit_time',)
ordering_csv = ('submit_time',) # keep legacy CSV ordering
orderable_fields = ('id', 'submit_time',) # used to validate ordering in URL
select_date_form = None
def dispatch(self, request, *args, **kwargs):
""" Check permissions and set the form page """
self.form_page = kwargs.get('form_page')
if not get_forms_for_user(request.user).filter(pk=self.form_page.id).exists():
raise PermissionDenied
self.is_csv_export = (self.request.GET.get('action') == 'CSV')
if self.is_csv_export:
self.paginate_by = None
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
""" Return queryset of form submissions with filter and order_by applied """
submission_class = self.form_page.get_submission_class()
queryset = submission_class._default_manager.filter(page=self.form_page)
filtering = self.get_filtering()
if filtering and isinstance(filtering, dict):
queryset = queryset.filter(**filtering)
ordering = self.get_ordering()
if ordering:
if isinstance(ordering, str):
ordering = (ordering,)
queryset = queryset.order_by(*ordering)
return queryset
def get_paginate_by(self, queryset):
""" Get the number of items to paginate by, or ``None`` for no pagination """
if self.is_csv_export:
return None
return self.paginate_by
def get_validated_ordering(self):
""" Return a dict of field names with ordering labels if ordering is valid """
orderable_fields = self.orderable_fields or ()
ordering = dict()
if self.is_csv_export:
# Revert to CSV order_by submit_time ascending for backwards compatibility
default_ordering = self.ordering_csv or ()
else:
default_ordering = self.ordering or ()
if isinstance(default_ordering, str):
default_ordering = (default_ordering,)
ordering_strs = self.request.GET.getlist('order_by') or list(default_ordering)
for order in ordering_strs:
try:
_, prefix, field_name = order.rpartition('-')
if field_name in orderable_fields:
ordering[field_name] = (
prefix, 'descending' if prefix == '-' else 'ascending'
)
except (IndexError, ValueError):
continue # invalid ordering specified, skip it
return ordering
def get_ordering(self):
""" Return the field or fields to use for ordering the queryset """
ordering = self.get_validated_ordering()
return [values[0] + name for name, values in ordering.items()]
def get_filtering(self):
""" Return filering as a dict for submissions queryset """
self.select_date_form = SelectDateForm(self.request.GET)
result = dict()
if self.select_date_form.is_valid():
date_from = self.select_date_form.cleaned_data.get('date_from')
date_to = self.select_date_form.cleaned_data.get('date_to')
if date_to:
# careful: date_to must be increased by 1 day
# as submit_time is a time so will always be greater
date_to += datetime.timedelta(days=1)
if date_from:
result['submit_time__range'] = [date_from, date_to]
else:
result['submit_time__lte'] = date_to
elif date_from:
result['submit_time__gte'] = date_from
return result
def get_csv_filename(self):
""" Returns the filename for the generated CSV file """
return 'export-{}.csv'.format(
datetime.datetime.today().strftime('%Y-%m-%d')
)
def get_csv_response(self, context):
""" Returns a CSV response """
filename = self.get_csv_filename()
response = HttpResponse(content_type='text/csv; charset=utf-8')
response['Content-Disposition'] = 'attachment;filename=export.csv'
# Prevents UnicodeEncodeError for labels with non-ansi symbols
data_headings = [smart_str(label) for label in data_headings]
response['Content-Disposition'] = 'attachment;filename={}'.format(filename)
writer = csv.writer(response)
writer.writerow(data_headings)
for s in submissions:
data_row = []
form_data = s.get_data()
for name, label in data_fields:
val = form_data.get(name)
if isinstance(val, list):
val = ', '.join(val)
data_row.append(smart_str(val))
writer.writerow(context['data_headings'])
for data_row in context['data_rows']:
writer.writerow(data_row)
return response
paginator, submissions = paginate(request, submissions)
def render_to_response(self, context, **response_kwargs):
if self.is_csv_export:
return self.get_csv_response(context)
return super().render_to_response(context, **response_kwargs)
data_rows = []
for s in submissions:
form_data = s.get_data()
data_row = []
for name, label in data_fields:
val = form_data.get(name)
if isinstance(val, list):
val = ', '.join(val)
data_row.append(val)
data_rows.append({
"model_id": s.id,
"fields": data_row
def get_context_data(self, **kwargs):
""" Return context for view, handle CSV or normal output """
context = super().get_context_data(**kwargs)
submissions = context[self.context_object_name]
data_fields = self.form_page.get_data_fields()
data_rows = []
if self.is_csv_export:
# Build data_rows as list of lists containing formatted data values
# Using smart_str prevents UnicodeEncodeError for values with non-ansi symbols
for submission in submissions:
form_data = submission.get_data()
data_row = []
for name, label in data_fields:
val = form_data.get(name)
if isinstance(val, list):
val = ', '.join(val)
data_row.append(smart_str(val))
data_rows.append(data_row)
data_headings = [smart_str(label) for name, label in data_fields]
else:
# Build data_rows as list of dicts containing model_id and fields
for submission in submissions:
form_data = submission.get_data()
data_row = []
for name, label in data_fields:
val = form_data.get(name)
if isinstance(val, list):
val = ', '.join(val)
data_row.append(val)
data_rows.append({
'model_id': submission.id,
'fields': data_row
})
# Build data_headings as list of dicts containing model_id and fields
ordering_by_field = self.get_validated_ordering()
orderable_fields = self.orderable_fields
data_headings = []
for name, label in data_fields:
order_label = None
if name in orderable_fields:
order = ordering_by_field.get(name)
if order:
order_label = order[1] # 'ascending' or 'descending'
else:
order_label = 'orderable' # not ordered yet but can be
data_headings.append({
'name': name,
'label': label,
'order': order_label,
})
context.update({
'form_page': self.form_page,
'select_date_form': self.select_date_form,
'data_headings': data_headings,
'data_rows': data_rows,
'submissions': submissions,
})
return render(request, 'wagtailforms/index_submissions.html', {
'form_page': form_page,
'select_date_form': select_date_form,
'submissions': submissions,
'data_fields_with_ordering': data_fields_with_ordering,
'data_rows': data_rows
})
return context

View File

@ -3,9 +3,9 @@ from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from wagtail.admin.menu import MenuItem
from wagtail.core import hooks
from wagtail.contrib.forms import urls
from wagtail.contrib.forms.models import get_forms_for_user
from wagtail.contrib.forms.utils import get_forms_for_user
from wagtail.core import hooks
@hooks.register('register_admin_urls')

View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-12-07 07:57
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields
import wagtail.core.fields
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0040_page_draft_title'),
('tests', '0024_tableblockstreampage'),
]
operations = [
migrations.CreateModel(
name='FormFieldForCustomListViewPage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
('label', models.CharField(help_text='The label of the form field', max_length=255, verbose_name='label')),
('field_type', models.CharField(choices=[('singleline', 'Single line text'), ('multiline', 'Multi-line text'), ('email', 'Email'), ('number', 'Number'), ('url', 'URL'), ('checkbox', 'Checkbox'), ('checkboxes', 'Checkboxes'), ('dropdown', 'Drop down'), ('multiselect', 'Multiple select'), ('radio', 'Radio buttons'), ('date', 'Date'), ('datetime', 'Date/time'), ('hidden', 'Hidden field')], max_length=16, verbose_name='field type')),
('required', models.BooleanField(default=True, verbose_name='required')),
('choices', models.TextField(blank=True, help_text='Comma separated list of choices. Only applicable in checkboxes, radio and dropdown.', verbose_name='choices')),
('default_value', models.CharField(blank=True, help_text='Default value. Comma separated values supported for checkboxes.', max_length=255, verbose_name='default value')),
('help_text', models.CharField(blank=True, max_length=255, verbose_name='help text')),
],
options={
'abstract': False,
'ordering': ['sort_order'],
},
),
migrations.CreateModel(
name='FormPageWithCustomSubmissionListView',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
('to_address', models.CharField(blank=True, help_text='Optional - form submissions will be emailed to these addresses. Separate multiple addresses by comma.', max_length=255, verbose_name='to address')),
('from_address', models.CharField(blank=True, max_length=255, verbose_name='from address')),
('subject', models.CharField(blank=True, max_length=255, verbose_name='subject')),
('intro', wagtail.core.fields.RichTextField(blank=True)),
('thank_you_text', wagtail.core.fields.RichTextField(blank=True)),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
migrations.AddField(
model_name='formfieldforcustomlistviewpage',
name='page',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='form_fields', to='tests.FormPageWithCustomSubmissionListView'),
),
]

View File

@ -37,6 +37,8 @@ from wagtail.snippets.edit_handlers import SnippetChooserPanel
from wagtail.snippets.models import register_snippet
from .forms import ValidatedPageForm
from .views import CustomSubmissionsListView
EVENT_AUDIENCE_CHOICES = (
('public', "Public"),
@ -566,6 +568,48 @@ class CustomFormPageSubmission(AbstractFormSubmission):
return form_data
# Custom form page with custom submission listing view and form submission
class FormFieldForCustomListViewPage(AbstractFormField):
page = ParentalKey(
'FormPageWithCustomSubmissionListView',
related_name='form_fields',
on_delete=models.CASCADE
)
class FormPageWithCustomSubmissionListView(AbstractEmailForm):
"""Form Page with customised submissions listing view"""
intro = RichTextField(blank=True)
thank_you_text = RichTextField(blank=True)
submissions_list_view_class = CustomSubmissionsListView
def get_submission_class(self):
return CustomFormPageSubmission
def get_data_fields(self):
data_fields = [
('username', 'Username'),
]
data_fields += super().get_data_fields()
return data_fields
content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('intro', classname="full"),
InlinePanel('form_fields', label="Form fields"),
FieldPanel('thank_you_text', classname="full"),
MultiFieldPanel([
FieldPanel('to_address', classname="full"),
FieldPanel('from_address', classname="full"),
FieldPanel('subject', classname="full"),
], "Email")
]
# Snippets
class AdvertPlacement(models.Model):
page = ParentalKey('wagtailcore.Page', related_name='advert_placements', on_delete=models.CASCADE)

View File

@ -4,6 +4,7 @@ from django.template.response import TemplateResponse
from wagtail.admin import messages
from wagtail.admin.utils import user_passes_test
from wagtail.contrib.forms.views import SubmissionsListView
def user_is_called_bob(user):
@ -22,3 +23,14 @@ def message_test(request):
return redirect('testapp_message_test')
else:
return TemplateResponse(request, 'wagtailadmin/base.html')
class CustomSubmissionsListView(SubmissionsListView):
paginate_by = 50
ordering = ('submit_time',)
ordering_csv = ('-submit_time',)
def get_csv_filename(self):
""" Returns the filename for CSV file with page title at start"""
filename = super().get_csv_filename()
return self.form_page.slug + '-' + filename