diff --git a/docs/reference/hooks.rst b/docs/reference/hooks.rst index 7c4e0d629..1733cc20a 100644 --- a/docs/reference/hooks.rst +++ b/docs/reference/hooks.rst @@ -215,7 +215,36 @@ Hooks for building new areas of the admin interface (alongside pages, images, do ``register_permissions`` ~~~~~~~~~~~~~~~~~~~~~~~~ - Return a queryset of Permission objects to be shown in the Groups administration area. + Return a queryset of ``Permission`` objects to be shown in the Groups administration area. + + +.. _filter_form_submissions_for_user: + +``filter_form_submissions_for_user`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Allows access to form submissions to be customised on a per-user, per-form basis. + + This hook takes two parameters: + - The user attempting to access form submissions + - A ``QuerySet`` of form pages + + The hook must return a ``QuerySet`` containing a subset of these form pages which the user is allowed to access the submissions for. + + For example, to prevent non-superusers from accessing form submissions: + + .. code-block:: python + + from wagtail.wagtailcore import hooks + + + @hooks.register('filter_form_submissions_for_user') + def construct_forms_for_user(user, queryset): + if not user.is_superuser: + queryset = queryset.none() + + return queryset + Editor interface diff --git a/wagtail/wagtailforms/models.py b/wagtail/wagtailforms/models.py index cecf7851c..9f33633fd 100644 --- a/wagtail/wagtailforms/models.py +++ b/wagtail/wagtailforms/models.py @@ -15,6 +15,7 @@ from unidecode import unidecode from wagtail.wagtailadmin.edit_handlers import FieldPanel from wagtail.wagtailadmin.utils import send_mail +from wagtail.wagtailcore import hooks from wagtail.wagtailcore.models import Orderable, Page, UserPagePermissionsProxy, get_page_models from .forms import FormBuilder, WagtailAdminFormPageForm @@ -140,8 +141,14 @@ def get_forms_for_user(user): """ Return a queryset of form pages that this user is allowed to access the submissions for """ - editable_pages = UserPagePermissionsProxy(user).editable_pages() - return editable_pages.filter(content_type__in=get_form_types()) + 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): diff --git a/wagtail/wagtailforms/tests/test_views.py b/wagtail/wagtailforms/tests/test_views.py index 7c9ce8e22..4b730b9ad 100644 --- a/wagtail/wagtailforms/tests/test_views.py +++ b/wagtail/wagtailforms/tests/test_views.py @@ -50,7 +50,7 @@ class TestFormResponsesPanel(TestCase): self.assertEqual('', result) -class TestFormsIndex(TestCase): +class TestFormsIndex(TestCase, WagtailTestUtils): fixtures = ['test.json'] def setUp(self): @@ -133,6 +133,25 @@ class TestFormsIndex(TestCase): # Check that the user can see the form page self.assertIn(self.form_page, response.context['form_pages']) + def test_cant_see_forms_after_filter_form_submissions_for_user_hook(self): + # Hook allows to see forms only to superusers + def construct_forms_for_user(user, queryset): + if not user.is_superuser: + queryset = queryset.none() + + return queryset + + response = self.client.get(reverse('wagtailforms:index')) + + # Check that an user can see the form page + self.assertIn(self.form_page, response.context['form_pages']) + + with self.register_hook('filter_form_submissions_for_user', construct_forms_for_user): + response = self.client.get(reverse('wagtailforms:index')) + + # Check that an user can't see the form page + self.assertNotIn(self.form_page, response.context['form_pages']) + # TODO: Rename to TestFormsSubmissionsList class TestFormsSubmissions(TestCase, WagtailTestUtils): @@ -185,6 +204,24 @@ class TestFormsSubmissions(TestCase, WagtailTestUtils): self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html') self.assertEqual(len(response.context['data_rows']), 2) + def test_list_submissions_after_filter_form_submissions_for_user_hook(self): + # Hook forbids to delete form submissions for everyone + def construct_forms_for_user(user, queryset): + return queryset.none() + + response = self.client.get(reverse('wagtailforms:list_submissions', args=(self.form_page.id,))) + + # An user can see form submissions without the hook + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html') + self.assertEqual(len(response.context['data_rows']), 2) + + with self.register_hook('filter_form_submissions_for_user', construct_forms_for_user): + response = self.client.get(reverse('wagtailforms:list_submissions', args=(self.form_page.id,))) + + # An user cant' see form submissions with the hook + self.assertEqual(response.status_code, 403) + def test_list_submissions_filtering_date_from(self): response = self.client.get( reverse('wagtailforms:list_submissions', args=(self.form_page.id,)), {'date_from': '01/01/2014'} @@ -268,6 +305,33 @@ class TestFormsSubmissions(TestCase, WagtailTestUtils): self.assertEqual(data_lines[1], '2013-01-01 12:00:00+00:00,old@example.com,this is a really old message,None\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_export_after_filter_form_submissions_for_user_hook(self): + # Hook forbids to delete form submissions for everyone + def construct_forms_for_user(user, queryset): + return queryset.none() + + response = self.client.get( + reverse('wagtailforms:list_submissions', args=(self.form_page.id,)), + {'action': 'CSV'} + ) + + # An user can export form submissions without the hook + self.assertEqual(response.status_code, 200) + data_lines = response.content.decode().split("\n") + + self.assertEqual(data_lines[0], 'Submission date,Your email,Your message,Your choices\r') + self.assertEqual(data_lines[1], '2013-01-01 12:00:00+00:00,old@example.com,this is a really old message,None\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') + + with self.register_hook('filter_form_submissions_for_user', construct_forms_for_user): + response = self.client.get( + reverse('wagtailforms:list_submissions', args=(self.form_page.id,)), + {'action': 'CSV'} + ) + + # An user can't export form submission with the hook + self.assertEqual(response.status_code, 403) + def test_list_submissions_csv_export_with_date_from_filtering(self): response = self.client.get( reverse('wagtailforms:list_submissions', args=(self.form_page.id,)), @@ -365,7 +429,7 @@ class TestFormsSubmissions(TestCase, WagtailTestUtils): # TODO: add TestCustomFormsSubmissionsList -class TestDeleteFormSubmission(TestCase): +class TestDeleteFormSubmission(TestCase, WagtailTestUtils): fixtures = ['test.json'] def setUp(self): @@ -408,6 +472,29 @@ class TestDeleteFormSubmission(TestCase): # Check that the deletion has not happened self.assertEqual(FormSubmission.objects.count(), 2) + def test_delete_submission_after_filter_form_submissions_for_user_hook(self): + # Hook forbids to delete form submissions for everyone + def construct_forms_for_user(user, queryset): + return queryset.none() + + 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) + )) + + # An user can't delete a from submission with the hook + self.assertEqual(response.status_code, 403) + self.assertEqual(FormSubmission.objects.count(), 2) + + # 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) + )) + self.assertEqual(FormSubmission.objects.count(), 1) + self.assertRedirects(response, reverse("wagtailforms:list_submissions", args=(self.form_page.id,))) + # TODO: add TestDeleteCustomFormSubmission