Repository: mozilla/pontoon

Issue:
Allow admins to disable users from their profile, and provide a motivation
Sometimes we have users abusing Pontoon, and the only approach available at the moment is to remove the `Active` flag via Django admin.

There is already #2896 on file to make the information clear when looking at their profile, but I think we also need to make the process easier for admins:
* It should be possible to disable the user directly from the profile, and provide a motivation.
* The motivation will be displayed to the user when they log in (not sure what happens at the moment?), with information on how to reach us to appeal the decision.

This would be very similar to what Bugzilla does.
Allow admins to disable users from their profile, and provide a motivation
Sometimes we have users abusing Pontoon, and the only approach available at the moment is to remove the `Active` flag via Django admin.

There is already #2896 on file to make the information clear when looking at their profile, but I think we also need to make the process easier for admins:
* It should be possible to disable the user directly from the profile, and provide a motivation.
* The motivation will be displayed to the user when they log in (not sure what happens at the moment?), with information on how to reach us to appeal the decision.

This would be very similar to what Bugzilla does.


Discussion:
> The motivation will be displayed to the user when they log in (not sure what happens at the moment?), with information on how to reach us to appeal the decision.

Deactivated users are unable to log in.
> The motivation will be displayed to the user when they log in (not sure what happens at the moment?), with information on how to reach us to appeal the decision.

Deactivated users are unable to log in.

Resolving patch:
diff --git a/pontoon/contributors/urls.py b/pontoon/contributors/urls.py
--- a/pontoon/contributors/urls.py
+++ b/pontoon/contributors/urls.py
@@ -104,4 +104,10 @@ class UsernameConverter(StringConverter):
         views.update_contribution_timeline,
         name="pontoon.contributors.update_contribution_timeline",
     ),
+    # AJAX: Toggle user account status (i.e. `is_active`)
+    path(
+        "toggle-active-user-status/<username:username>/",
+        views.toggle_active_user_status,
+        name="pontoon.contributors.toggle_active_user_status",
+    ),
 ]
diff --git a/pontoon/contributors/views.py b/pontoon/contributors/views.py
--- a/pontoon/contributors/views.py
+++ b/pontoon/contributors/views.py
@@ -471,6 +471,26 @@ def mark_all_notifications_as_read(request):
     return JsonResponse({"status": True})
 
 
+@login_required(redirect_field_name="", login_url="/403")
+@require_POST
+@transaction.atomic
+def toggle_active_user_status(request, username):
+    # only admins are authorized to (dis|en)able users
+    if not request.user.is_superuser:
+        return JsonResponse(
+            {
+                "status": False,
+                "message": "Forbidden: You don't have permission to change user active status.",
+            },
+            status=403,
+        )
+
+    user = get_object_or_404(User, username=username)
+    user.is_active = not user.is_active
+    user.save(update_fields=["is_active"])
+    return JsonResponse({"status": True})
+
+
 class ContributorsMixin:
     def contributors_filter(self, **kwargs):
         """


Test patch:
diff --git a/pontoon/contributors/tests/test_views.py b/pontoon/contributors/tests/test_views.py
--- a/pontoon/contributors/tests/test_views.py
+++ b/pontoon/contributors/tests/test_views.py
@@ -257,3 +257,41 @@ def test_given_period(member, mock_contributors_render):
     ):
         member.client.get("/contributors/?period=6")
         assert mock_contributors_render.call_args[0][0]["period"] == 6
+
+
+@pytest.mark.django_db
+def test_toggle_active_user_status(client_superuser, user_a):
+    url = reverse(
+        "pontoon.contributors.toggle_active_user_status", args=[user_a.username]
+    )
+
+    # request on active user --> user disabled
+    assert user_a.is_active is True
+    response = client_superuser.post(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+    assert response.status_code == 200, User.objects.get(pk=user_a).is_active is False
+
+    # request on disabled user --> user enabled
+    response = client_superuser.post(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+    assert response.status_code == 200, User.objects.get(pk=user_a).is_active is True
+
+
+@pytest.mark.django_db
+def test_toggle_active_user_status_user_not_found(client_superuser):
+    url = reverse("pontoon.contributors.toggle_active_user_status", args=["unknown"])
+    response = client_superuser.post(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+    assert response.status_code == 404
+
+
+@pytest.mark.django_db
+def test_toggle_active_user_status_requires_admin(member, admin, client_superuser):
+    url = reverse(
+        "pontoon.contributors.toggle_active_user_status", args=[member.user.username]
+    )
+
+    member.client.force_login(member.user)
+    response = member.client.post(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+    assert response.status_code == 403
+
+    member.client.force_login(admin)
+    response = client_superuser.post(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+    assert response.status_code == 200