From 7859627a6e9fbc30e389ed6ea48f1ac9e194bf51 Mon Sep 17 00:00:00 2001 From: Jonathan Carmack Date: Thu, 13 Jul 2017 20:46:33 -0400 Subject: [PATCH] Added hooks for user for create, delete, and edit actions --- CHANGELOG.txt | 1 + docs/reference/hooks.rst | 84 +++++++++++++++ docs/releases/2.0.rst | 1 + wagtail/wagtailusers/tests.py | 154 ++++++++++++++++++++++++++++ wagtail/wagtailusers/views/users.py | 25 +++++ 5 files changed, 265 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 41ad416c01..73788f6ab7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -15,6 +15,7 @@ Changelog * Moved usage count to the sidebar on the edit page (Kees Hink) * Explorer menu now reflects customisations to the page listing made via the `construct_explorer_page_queryset` hook and `ModelAdmin.exclude_from_explorer` property (Tim Heap) * "Choose another image" button changed to "Change image" to avoid ambiguity (Edd Baldry) + * Added hooks `before_create_user`, `after_create_user`, `before_delete_user`, `after_delete_user`, `before_edit_user`, `after_edit_user` (Jon Carmack) * 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) diff --git a/docs/reference/hooks.rst b/docs/reference/hooks.rst index 7431ad2747..fc05bf5763 100644 --- a/docs/reference/hooks.rst +++ b/docs/reference/hooks.rst @@ -513,6 +513,90 @@ Hooks for customising the way users are directed through the process of creating return items.append( UserbarPuppyLinkItem() ) +Admin workflow +-------------- +Hooks for customising the way admins are directed through the process of editing users. + + +.. _after_create_user: + +``after_create_user`` +~~~~~~~~~~~~~~~~~~~~~ + + Do something with a ``User`` object after it has been saved to the database. The callable passed to this hook should take a ``request`` object and a ``user`` object. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the User index page. + + .. code-block:: python + + from django.http import HttpResponse + + from wagtail.wagtailcore import hooks + + @hooks.register('after_create_user') + def do_after_page_create(request, user): + return HttpResponse("Congrats on creating a new user!", content_type="text/plain") + + +.. _before_create_user: + +``before_create_user`` +~~~~~~~~~~~~~~~~~~~~~~ + + Called at the beginning of the "create user" view passing in the request. + + The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view. + + Unlike, ``after_create_user``, this is run both for both ``GET`` and ``POST`` requests. + + This can be used to completely override the user editor on a per-view basis: + + .. code-block:: python + + from wagtail.wagtailcore import hooks + + from .models import AwesomePage + from .admin_views import edit_awesome_page + + @hooks.register('before_create_user') + def before_create_page(request): + return HttpResponse("A user creation form", content_type="text/plain") + + + +.. _after_delete_user: + +``after_delete_user`` +~~~~~~~~~~~~~~~~~~~~~ + + Do something after a ``User`` object is deleted. Uses the same behavior as ``after_create_user``. + + +.. _before_delete_user: + +``before_delete_user`` +~~~~~~~~~~~~~~~~~~~~~~ + + Called at the beginning of the "delete user" view passing in the request and the user object. + + Uses the same behavior as ``before_create_user``. + + +.. _after_edit_user: + +``after_edit_user`` +~~~~~~~~~~~~~~~~~~~ + + Do something with a ``User`` object after it has been updated. Uses the same behavior as ``after_create_user``. + + +.. _before_edit_user: + +``before_edit_user`` +~~~~~~~~~~~~~~~~~~~~~ + + Called at the beginning of the "edit user" view passing in the request and the user object. + + Uses the same behavior as ``before_create_user``. + Choosers -------- diff --git a/docs/releases/2.0.rst b/docs/releases/2.0.rst index e0d277143c..b9c5d65198 100644 --- a/docs/releases/2.0.rst +++ b/docs/releases/2.0.rst @@ -23,6 +23,7 @@ Other features * Moved usage count to the sidebar on the edit page (Kees Hink) * Explorer menu now reflects customisations to the page listing made via the `construct_explorer_page_queryset` hook and `ModelAdmin.exclude_from_explorer` property (Tim Heap) * "Choose another image" button changed to "Change image" to avoid ambiguity (Edd Baldry) + * Added hooks ``before_create_user``, ``after_create_user``, ``before_delete_user``, ``after_delete_user``, ``before_edit_user``, ``after_edit_user`` (Jon Carmack) Bug fixes ~~~~~~~~~ diff --git a/wagtail/wagtailusers/tests.py b/wagtail/wagtailusers/tests.py index ec797c93ec..8f188fb77f 100644 --- a/wagtail/wagtailusers/tests.py +++ b/wagtail/wagtailusers/tests.py @@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission from django.core.exceptions import ImproperlyConfigured from django.core.files.uploadedfile import SimpleUploadedFile +from django.http import HttpRequest, HttpResponse from django.test import TestCase, override_settings from django.urls import reverse from django.utils import six @@ -328,6 +329,61 @@ class TestUserCreateView(TestCase, WagtailTestUtils): self.assertEqual(users.first().email, 'test@user.com') self.assertFalse(users.first().has_usable_password()) + def test_before_create_user_hook(self): + def hook_func(request): + self.assertIsInstance(request, HttpRequest) + return HttpResponse("Overridden!") + + with self.register_hook('before_create_user', hook_func): + response = self.client.get( + reverse('wagtailusers_users:add') + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"Overridden!") + + def test_before_create_user_hook_post(self): + def hook_func(request): + self.assertIsInstance(request, HttpRequest) + return HttpResponse("Overridden!") + + with self.register_hook('before_create_user', hook_func): + post_data = { + 'username': "testuser", + 'email': "testuser@test.com", + 'password1': 'password12', + 'password2': 'password12', + 'first_name': 'test', + 'last_name': 'user', + } + response = self.client.post( + reverse('wagtailusers_users:add'), + post_data + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"Overridden!") + + def test_after_create_user_hook(self): + def hook_func(request, user): + self.assertIsInstance(request, HttpRequest) + self.assertIsInstance(user, get_user_model()) + return HttpResponse("Overridden!") + + with self.register_hook('after_create_user', hook_func): + post_data = { + 'username': "testuser", + 'email': "testuser@test.com", + 'password1': 'password12', + 'password2': 'password12', + 'first_name': 'test', + 'last_name': 'user', + } + response = self.client.post( + reverse('wagtailusers_users:add'), + post_data + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"Overridden!") + class TestUserDeleteView(TestCase, WagtailTestUtils): def setUp(self): @@ -387,6 +443,45 @@ class TestUserDeleteView(TestCase, WagtailTestUtils): users = get_user_model().objects.filter(username='testsuperuser') self.assertEqual(users.count(), 0) + def test_before_delete_user_hook(self): + def hook_func(request, user): + self.assertIsInstance(request, HttpRequest) + self.assertEqual(user.pk, self.test_user.pk) + + return HttpResponse("Overridden!") + + with self.register_hook('before_delete_user', hook_func): + response = self.client.get(reverse('wagtailusers_users:delete', args=(self.test_user.pk, ))) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"Overridden!") + + def test_before_delete_user_hook_post(self): + def hook_func(request, user): + self.assertIsInstance(request, HttpRequest) + self.assertEqual(user.pk, self.test_user.pk) + + return HttpResponse("Overridden!") + + with self.register_hook('before_delete_user', hook_func): + response = self.client.post(reverse('wagtailusers_users:delete', args=(self.test_user.pk, ))) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"Overridden!") + + def test_after_delete_user_hook(self): + def hook_func(request, user): + self.assertIsInstance(request, HttpRequest) + self.assertEqual(user.username, self.test_user.username) + + return HttpResponse("Overridden!") + + with self.register_hook('after_delete_user', hook_func): + response = self.client.post(reverse('wagtailusers_users:delete', args=(self.test_user.pk, ))) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"Overridden!") + class TestUserDeleteViewForNonSuperuser(TestCase, WagtailTestUtils): def setUp(self): @@ -703,6 +798,65 @@ class TestUserEditView(TestCase, WagtailTestUtils): self.assertEqual(user.first_name, 'Edited') self.assertTrue(user.check_password('password')) + def test_before_edit_user_hook(self): + def hook_func(request, user): + self.assertIsInstance(request, HttpRequest) + self.assertEqual(user.pk, self.test_user.pk) + + return HttpResponse("Overridden!") + + with self.register_hook('before_edit_user', hook_func): + response = self.client.get(reverse('wagtailusers_users:edit', args=(self.test_user.pk, ))) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"Overridden!") + + def test_before_edit_user_hook_post(self): + def hook_func(request, user): + self.assertIsInstance(request, HttpRequest) + self.assertEqual(user.pk, self.test_user.pk) + + return HttpResponse("Overridden!") + + with self.register_hook('before_edit_user', hook_func): + post_data = { + 'username': "testuser", + 'email': "test@user.com", + 'first_name': "Edited", + 'last_name': "User", + 'password1': "password", + 'password2': "password", + } + response = self.client.post( + reverse('wagtailusers_users:edit', args=(self.test_user.pk, )), post_data + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"Overridden!") + + def test_after_edit_user_hook_post(self): + def hook_func(request, user): + self.assertIsInstance(request, HttpRequest) + self.assertEqual(user.pk, self.test_user.pk) + + return HttpResponse("Overridden!") + + with self.register_hook('after_edit_user', hook_func): + post_data = { + 'username': "testuser", + 'email': "test@user.com", + 'first_name': "Edited", + 'last_name': "User", + 'password1': "password", + 'password2': "password", + } + response = self.client.post( + reverse('wagtailusers_users:edit', args=(self.test_user.pk, )), post_data + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"Overridden!") + class TestUserProfileCreation(TestCase, WagtailTestUtils): def setUp(self): diff --git a/wagtail/wagtailusers/views/users.py b/wagtail/wagtailusers/views/users.py index f9e43ce71f..c9d564268f 100644 --- a/wagtail/wagtailusers/views/users.py +++ b/wagtail/wagtailusers/views/users.py @@ -15,6 +15,7 @@ from wagtail.wagtailadmin import messages from wagtail.wagtailadmin.forms import SearchForm from wagtail.wagtailadmin.utils import ( any_permission_required, permission_denied, permission_required) +from wagtail.wagtailcore import hooks from wagtail.wagtailcore.compat import AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME from wagtail.wagtailusers.forms import UserCreationForm, UserEditForm from wagtail.wagtailusers.utils import user_can_delete_user @@ -122,6 +123,10 @@ def index(request): @permission_required(add_user_perm) def create(request): + for fn in hooks.get_hooks('before_create_user'): + result = fn(request) + if hasattr(result, 'status_code'): + return result if request.method == 'POST': form = get_user_creation_form()(request.POST, request.FILES) if form.is_valid(): @@ -129,6 +134,10 @@ def create(request): messages.success(request, _("User '{0}' created.").format(user), buttons=[ messages.button(reverse('wagtailusers_users:edit', args=(user.pk,)), _('Edit')) ]) + for fn in hooks.get_hooks('after_create_user'): + result = fn(request, user) + if hasattr(result, 'status_code'): + return result return redirect('wagtailusers_users:index') else: messages.error(request, _("The user could not be created due to errors.")) @@ -146,6 +155,10 @@ def edit(request, user_id): can_delete = user_can_delete_user(request.user, user) editing_self = request.user == user + for fn in hooks.get_hooks('before_edit_user'): + result = fn(request, user) + if hasattr(result, 'status_code'): + return result if request.method == 'POST': form = get_user_edit_form()(request.POST, request.FILES, instance=user, editing_self=editing_self) if form.is_valid(): @@ -153,6 +166,10 @@ def edit(request, user_id): messages.success(request, _("User '{0}' updated.").format(user), buttons=[ messages.button(reverse('wagtailusers_users:edit', args=(user.pk,)), _('Edit')) ]) + for fn in hooks.get_hooks('after_edit_user'): + result = fn(request, user) + if hasattr(result, 'status_code'): + return result return redirect('wagtailusers_users:index') else: messages.error(request, _("The user could not be saved due to errors.")) @@ -173,9 +190,17 @@ def delete(request, user_id): if not user_can_delete_user(request.user, user): return permission_denied(request) + for fn in hooks.get_hooks('before_delete_user'): + result = fn(request, user) + if hasattr(result, 'status_code'): + return result if request.method == 'POST': user.delete() messages.success(request, _("User '{0}' deleted.").format(user)) + for fn in hooks.get_hooks('after_delete_user'): + result = fn(request, user) + if hasattr(result, 'status_code'): + return result return redirect('wagtailusers_users:index') return render(request, "wagtailusers/users/confirm_delete.html", {