From 658e12eb86daf237cd210bfc28ea42a25daf4bbd Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 8 Sep 2021 16:47:12 -0700 Subject: [PATCH 1/6] Allow admins to suspend pending users --- .../migrations/0090_auto_20210908_2346.py | 45 +++++++++++++++++++ bookwyrm/models/base_model.py | 1 + .../user_admin/user_moderation_actions.html | 19 ++++++-- bookwyrm/urls.py | 5 +++ bookwyrm/views/__init__.py | 9 +++- bookwyrm/views/reports.py | 15 ++++++- 6 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 bookwyrm/migrations/0090_auto_20210908_2346.py diff --git a/bookwyrm/migrations/0090_auto_20210908_2346.py b/bookwyrm/migrations/0090_auto_20210908_2346.py new file mode 100644 index 00000000..7c870857 --- /dev/null +++ b/bookwyrm/migrations/0090_auto_20210908_2346.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.4 on 2021-09-08 23:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0089_user_show_suggested_users"), + ] + + operations = [ + migrations.AlterField( + model_name="connector", + name="deactivation_reason", + field=models.CharField( + blank=True, + choices=[ + ("pending", "Pending"), + ("self_deletion", "Self Deletion"), + ("moderator_suspension", "Moderator Suspension"), + ("moderator_deletion", "Moderator Deletion"), + ("domain_block", "Domain Block"), + ], + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="user", + name="deactivation_reason", + field=models.CharField( + blank=True, + choices=[ + ("pending", "Pending"), + ("self_deletion", "Self Deletion"), + ("moderator_suspension", "Moderator Suspension"), + ("moderator_deletion", "Moderator Deletion"), + ("domain_block", "Domain Block"), + ], + max_length=255, + null=True, + ), + ), + ] diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 5b55ea50..f19ebfd9 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -13,6 +13,7 @@ DeactivationReason = models.TextChoices( [ "pending", "self_deletion", + "moderator_suspension", "moderator_deletion", "domain_block", ], diff --git a/bookwyrm/templates/user_admin/user_moderation_actions.html b/bookwyrm/templates/user_admin/user_moderation_actions.html index e4460a68..3a82d92c 100644 --- a/bookwyrm/templates/user_admin/user_moderation_actions.html +++ b/bookwyrm/templates/user_admin/user_moderation_actions.html @@ -8,17 +8,28 @@

{% trans "Actions" %}

+ {% if user.is_active %}

{% trans "Send direct message" %}

+ {% endif %} + + {% if user.is_active or user.deactivation_reason == "pending" %} +
{% csrf_token %} - {% if user.is_active %} - {% else %} - - {% endif %}
+ + {% else %} + +
+ {% csrf_token %} + +
+ + {% endif %} +
{% if user.local %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 9ae1b822..b2b7723b 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -153,6 +153,11 @@ urlpatterns = [ views.suspend_user, name="settings-report-suspend", ), + re_path( + r"^settings/reports/(?P\d+)/unsuspend/?$", + views.unsuspend_user, + name="settings-report-unsuspend", + ), re_path( r"^settings/reports/(?P\d+)/resolve/?$", views.resolve_report, diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 5142d532..6d283b94 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -33,7 +33,14 @@ from .reading import edit_readthrough, create_readthrough from .reading import delete_readthrough, delete_progressupdate from .reading import ReadingStatus from .register import Register, ConfirmEmail, ConfirmEmailCode, resend_link -from .reports import Report, Reports, make_report, resolve_report, suspend_user +from .reports import ( + Report, + Reports, + make_report, + resolve_report, + suspend_user, + unsuspend_user, +) from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword from .search import Search diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index 46c23884..08330fa9 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -77,7 +77,20 @@ class Report(View): def suspend_user(_, user_id): """mark an account as inactive""" user = get_object_or_404(models.User, id=user_id) - user.is_active = not user.is_active + user.is_active = False + user.deactivation_reason = "moderator_suspension" + # this isn't a full deletion, so we don't want to tell the world + user.save(broadcast=False) + return redirect("settings-user", user.id) + + +@login_required +@permission_required("bookwyrm_moderate_user") +def unsuspend_user(_, user_id): + """mark an account as inactive""" + user = get_object_or_404(models.User, id=user_id) + user.is_active = True + user.deactivation_reason = None # this isn't a full deletion, so we don't want to tell the world user.save(broadcast=False) return redirect("settings-user", user.id) From 31e6e59047496f7dda0f1cde67929c8190c4639a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 8 Sep 2021 16:48:23 -0700 Subject: [PATCH 2/6] Updates test --- bookwyrm/tests/views/test_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py index 1b0b6008..de3f0379 100644 --- a/bookwyrm/tests/views/test_reports.py +++ b/bookwyrm/tests/views/test_reports.py @@ -134,6 +134,6 @@ class ReportViews(TestCase): self.assertFalse(self.rat.is_active) # re-activate - views.suspend_user(request, self.rat.id) + views.unsuspend_user(request, self.rat.id) self.rat.refresh_from_db() self.assertTrue(self.rat.is_active) From 916be2552dcf28555c7121f12f5e32b430d19a2e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 8 Sep 2021 16:58:16 -0700 Subject: [PATCH 3/6] View for moderators deleting users --- bookwyrm/views/edit_user.py | 1 - bookwyrm/views/reports.py | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/bookwyrm/views/edit_user.py b/bookwyrm/views/edit_user.py index c74b00c9..b97f2737 100644 --- a/bookwyrm/views/edit_user.py +++ b/bookwyrm/views/edit_user.py @@ -55,7 +55,6 @@ class DeleteUser(View): def post(self, request): """les get fancy with images""" form = forms.DeleteUserForm(request.POST, instance=request.user) - form.is_valid() # idk why but I couldn't get check_password to work on request.user user = models.User.objects.get(id=request.user.id) if form.is_valid() and user.check_password(form.cleaned_data["password"]): diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index 08330fa9..8246ae15 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -96,6 +96,26 @@ def unsuspend_user(_, user_id): return redirect("settings-user", user.id) +@login_required +@permission_required("bookwyrm_moderate_user") +def moderator_delete_user(request, user_id): + """permanently delete a user""" + user = get_object_or_404(models.User, id=user_id) + form = forms.DeleteUserForm(request.POST, instance=user) + + moderator = models.User.objects.get(id=request.user.id) + # check the moderator's password + if form.is_valid() and moderator.check_password(form.cleaned_data["password"]): + user.deactivation_reason = "moderator_deletion" + user.delete() + return redirect("settings-user", user.id) + + form.errors["password"] = ["Invalid password"] + + data = {"user": user, "group_form": forms.UserGroupForm(), "form": form} + return TemplateResponse(request, "user_admin/user.html", data) + + @login_required @permission_required("bookwyrm_moderate_post") def resolve_report(_, report_id): From 15344b6a8e8245880a20b818fe5e08c43ed17364 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 8 Sep 2021 17:21:45 -0700 Subject: [PATCH 4/6] Let moderators delete users --- .../user_admin/user_moderation_actions.html | 22 +++++++++++++------ bookwyrm/urls.py | 5 +++++ bookwyrm/views/__init__.py | 1 + bookwyrm/views/reports.py | 6 +++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/bookwyrm/templates/user_admin/user_moderation_actions.html b/bookwyrm/templates/user_admin/user_moderation_actions.html index 3a82d92c..12b70d3c 100644 --- a/bookwyrm/templates/user_admin/user_moderation_actions.html +++ b/bookwyrm/templates/user_admin/user_moderation_actions.html @@ -1,6 +1,6 @@ {% load i18n %}
- {% if not user.is_active and user.deactivation_reason == "self_deletion" %} + {% if not user.is_active and user.deactivation_reason == "self_deletion" or user.deactivation_reason == "moderator_deletion" %}
{% trans "Permanently deleted" %}
@@ -15,23 +15,31 @@ {% endif %} {% if user.is_active or user.deactivation_reason == "pending" %} - -
+ {% csrf_token %}
- {% else %} - -
+ {% csrf_token %}
- {% endif %} + {% if user.local %} +
+ {% trans "Permanently delete user" as button_text %} + {% include "snippets/toggle/open_button.html" with controls_text="delete_user" text=button_text class="is-danger is-light" %} +
+ {% endif %}
+ {% if user.local %} +
+ {% include "user_admin/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %} +
+ {% endif %} + {% if user.local %}
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index b2b7723b..460f5df9 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -158,6 +158,11 @@ urlpatterns = [ views.unsuspend_user, name="settings-report-unsuspend", ), + re_path( + r"^settings/reports/(?P\d+)/delete/?$", + views.moderator_delete_user, + name="settings-delete-user", + ), re_path( r"^settings/reports/(?P\d+)/resolve/?$", views.resolve_report, diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 6d283b94..6c88bd9e 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -40,6 +40,7 @@ from .reports import ( resolve_report, suspend_user, unsuspend_user, + moderator_delete_user, ) from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index 8246ae15..72b7f4db 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -1,5 +1,6 @@ """ moderation via flagged posts and users """ from django.contrib.auth.decorators import login_required, permission_required +from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator @@ -101,6 +102,11 @@ def unsuspend_user(_, user_id): def moderator_delete_user(request, user_id): """permanently delete a user""" user = get_object_or_404(models.User, id=user_id) + + # we can't delete users on other instances + if not user.local: + raise PermissionDenied + form = forms.DeleteUserForm(request.POST, instance=user) moderator = models.User.objects.get(id=request.user.id) From f5de1c903eedebf0d1283973deab26e4b560ee94 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 8 Sep 2021 17:33:43 -0700 Subject: [PATCH 5/6] Adds deletion test --- bookwyrm/tests/views/test_reports.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py index de3f0379..9fbeae04 100644 --- a/bookwyrm/tests/views/test_reports.py +++ b/bookwyrm/tests/views/test_reports.py @@ -1,4 +1,5 @@ """ test for app action functionality """ +import json from unittest.mock import patch from django.template.response import TemplateResponse from django.test import TestCase @@ -137,3 +138,23 @@ class ReportViews(TestCase): views.unsuspend_user(request, self.rat.id) self.rat.refresh_from_db() self.assertTrue(self.rat.is_active) + + @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") + @patch("bookwyrm.suggested_users.remove_user_task.delay") + def test_delete_user(self, *_): + """toggle whether a user is able to log in""" + self.assertTrue(self.rat.is_active) + request = self.factory.post("", {"password": "password"}) + request.user = self.local_user + request.user.is_superuser = True + + # de-activate + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + views.moderator_delete_user(request, self.rat.id) + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Delete") + + self.rat.refresh_from_db() + self.assertFalse(self.rat.is_active) + self.assertEqual(self.rat.deactivation_reason, "moderator_deletion") From fadcbbcec79fadd5be12633d942c4c6a0553e098 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 8 Sep 2021 17:34:01 -0700 Subject: [PATCH 6/6] Adds deletion form --- .../user_admin/delete_user_form.html | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 bookwyrm/templates/user_admin/delete_user_form.html diff --git a/bookwyrm/templates/user_admin/delete_user_form.html b/bookwyrm/templates/user_admin/delete_user_form.html new file mode 100644 index 00000000..0987bdf1 --- /dev/null +++ b/bookwyrm/templates/user_admin/delete_user_form.html @@ -0,0 +1,20 @@ +{% extends "components/inline_form.html" %} +{% load i18n %} + +{% block header %} +{% trans "Permanently delete user" %} +{% endblock %} + +{% block form %} + + {% csrf_token %} +
+ + + {% for error in form.password.errors %} +

{{ error | escape }}

+ {% endfor %} +
+ + +{% endblock %}