From 61a5e197cd990efdd7993470ff01a8f0b4ae3c3c Mon Sep 17 00:00:00 2001
From: Mehrdad Moradizadeh <mhrddmoradii@gmail.com>
Date: Wed, 14 Sep 2022 12:16:48 -0400
Subject: [PATCH] Refactor user edit view into class based view

- relates to #8622
---
 CHANGELOG.txt                                 |   1 +
 docs/releases/4.1.md                          |   2 +-
 .../templates/wagtailusers/users/edit.html    |   3 +-
 wagtail/users/urls/users.py                   |   2 +-
 wagtail/users/views/users.py                  | 124 ++++++++++--------
 5 files changed, 76 insertions(+), 56 deletions(-)

diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 8017275eeb..0ec233ada5 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -34,6 +34,7 @@ Changelog
  * Adopt `wagtail.admin.views.generic.IndexView` for the Users index listing and search results (Mehrdad Moradizadeh)
  * Adopt `wagtail.admin.views.generic.CreateView` for the User creation view (Mehrdad Moradizadeh)
  * Adopt `wagtail.admin.views.generic.DeleteView` for the User delete view (Mehrdad Moradizadeh)
+ * Adopt `wagtail.admin.views.generic.EditView` for the User edit view (Mehrdad Moradizadeh)
  * Fix: Prevent `PageQuerySet.not_public` from returning all pages when no page restrictions exist (Mehrdad Moradizadeh)
  * Fix: Ensure that duplicate block ids are unique when duplicating stream blocks in the page editor (Joshua Munn)
  * Fix: Revise colour usage so that privacy & locked indicators can be seen in Windows High Contrast mode (LB (Ben Johnston))
diff --git a/docs/releases/4.1.md b/docs/releases/4.1.md
index af147a8892..1a9c12ef5c 100644
--- a/docs/releases/4.1.md
+++ b/docs/releases/4.1.md
@@ -49,7 +49,7 @@ Snippet models that inherit from `DraftStateMixin` can now be assigned go-live a
  * Use `search` type input in documentation search (LB (Ben) Johnston)
  * Render `help_text` when set on `FieldPanel`, `MultiFieldPanel`, `FieldRowPanel`, and other panel APIs where it previously worked without official support (Matt Westcott)
  * Consolidate usage of Excel libraries to a single library `openpyxl`, removing usage of `XlsxWriter`, `tablib`, `xlrd` and `xlwt` (Jaap Roes)
- * Adopt generic class based views for the create User edit view, user delete view and Users index listing / search results (Mehrdad Moradizadeh)
+ * Adopt generic class based views for the create User create view, User edit view, user delete view and Users index listing / search results (Mehrdad Moradizadeh)
 
 ### Bug fixes
 
diff --git a/wagtail/users/templates/wagtailusers/users/edit.html b/wagtail/users/templates/wagtailusers/users/edit.html
index e4afc9810d..1ff0274050 100644
--- a/wagtail/users/templates/wagtailusers/users/edit.html
+++ b/wagtail/users/templates/wagtailusers/users/edit.html
@@ -1,7 +1,6 @@
-{% extends "wagtailadmin/base.html" %}
+{% extends "wagtailadmin/generic/base.html" %}
 {% load wagtailimages_tags %}
 {% load i18n %}
-{% block titletag %}{% trans "Editing" %} {{ user.get_username }}{% endblock %}
 {% block content %}
 
     {% trans "Editing" as editing_str %}
diff --git a/wagtail/users/urls/users.py b/wagtail/users/urls/users.py
index cc3003eb13..bfb695e87c 100644
--- a/wagtail/users/urls/users.py
+++ b/wagtail/users/urls/users.py
@@ -6,6 +6,6 @@ app_name = "wagtailusers_users"
 urlpatterns = [
     path("", users.Index.as_view(), name="index"),
     path("add/", users.Create.as_view(), name="add"),
-    path("<str:user_id>/", users.edit, name="edit"),
+    path("<str:pk>/", users.Edit.as_view(), name="edit"),
     path("<str:pk>/delete/", users.Delete.as_view(), name="delete"),
 ]
diff --git a/wagtail/users/views/users.py b/wagtail/users/views/users.py
index 5826845e95..e7c323e957 100644
--- a/wagtail/users/views/users.py
+++ b/wagtail/users/views/users.py
@@ -2,20 +2,15 @@ from django.conf import settings
 from django.contrib.auth import get_user_model, update_session_auth_hash
 from django.contrib.auth.models import Group
 from django.core.exceptions import PermissionDenied
-from django.db import transaction
 from django.db.models import Q
-from django.shortcuts import get_object_or_404, redirect
-from django.template.response import TemplateResponse
+from django.shortcuts import get_object_or_404
 from django.urls import reverse
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext_lazy
 
-from wagtail import hooks
 from wagtail.admin import messages
-from wagtail.admin.auth import permission_required
-from wagtail.admin.views.generic import CreateView, DeleteView, IndexView
+from wagtail.admin.views.generic import CreateView, DeleteView, EditView, IndexView
 from wagtail.compat import AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME
-from wagtail.log_actions import log
 from wagtail.permission_policies import ModelPermissionPolicy
 from wagtail.users.forms import UserCreationForm, UserEditForm
 from wagtail.users.utils import user_can_delete_user
@@ -179,57 +174,82 @@ class Create(CreateView):
         ]
 
 
-@permission_required(change_user_perm)
-def edit(request, user_id):
-    user = get_object_or_404(User, pk=user_id)
-    can_delete = user_can_delete_user(request.user, user)
-    editing_self = request.user == user
+class Edit(EditView):
+    """
+    Provide the ability to edit a user within the admin.
+    """
 
-    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
+    model = User
+    permission_policy = ModelPermissionPolicy(User)
+    form_class = get_user_edit_form()
+    template_name = "wagtailusers/users/edit.html"
+    index_url_name = "wagtailusers_users:index"
+    edit_url_name = "wagtailusers_users:edit"
+    delete_url_name = "wagtailusers_users:delete"
+    success_message = _("User '{0}' updated.")
+    context_object_name = "user"
+    error_message = gettext_lazy("The user could not be saved due to errors.")
+
+    def get_page_title(self):
+        return _("Editing %s") % self.object.get_username()
+
+    def get_page_subtitle(self):
+        return ""
+
+    def setup(self, request, *args, **kwargs):
+        super().setup(request, *args, **kwargs)
+        self.object = self.get_object()
+        self.can_delete = user_can_delete_user(request.user, self.object)
+        self.editing_self = request.user == self.object
+
+    def save_instance(self):
+        instance = super().save_instance()
+        if self.object == self.request.user and "password1" in self.form.changed_data:
+            # User is changing their own password; need to update their session hash
+            update_session_auth_hash(self.request, self.object)
+        return instance
+
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs.update(
+            {
+                "editing_self": self.editing_self,
+            }
         )
-        if form.is_valid():
-            with transaction.atomic():
-                user = form.save()
-                log(user, "wagtail.edit")
+        return kwargs
 
-            if user == request.user and "password1" in form.changed_data:
-                # User is changing their own password; need to update their session hash
-                update_session_auth_hash(request, user)
+    def run_before_hook(self):
+        return self.run_hook(
+            "before_edit_user",
+            self.request,
+            self.object,
+        )
 
-            messages.success(
-                request,
-                _("User '{0}' updated.").format(user),
-                buttons=[
-                    messages.button(
-                        reverse("wagtailusers_users:edit", args=(user.pk,)), _("Edit")
-                    )
-                ],
+    def run_after_hook(self):
+        return self.run_hook(
+            "after_edit_user",
+            self.request,
+            self.object,
+        )
+
+    def get_success_buttons(self):
+        return [
+            messages.button(
+                reverse(self.edit_url_name, args=(self.object.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."))
-    else:
-        form = get_user_edit_form()(instance=user, editing_self=editing_self)
+        ]
 
-    return TemplateResponse(
-        request,
-        "wagtailusers/users/edit.html",
-        {
-            "user": user,
-            "form": form,
-            "can_delete": can_delete,
-        },
-    )
+    def get_edit_url(self):
+        return reverse(self.edit_url_name, args=(self.object.pk,))
+
+    def get_delete_url(self):
+        return reverse(self.delete_url_name, args=(self.object.pk,))
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context.pop("action_url")
+        context["can_delete"] = self.can_delete
+        return context
 
 
 class Delete(DeleteView):