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/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 %} diff --git a/bookwyrm/templates/user_admin/user_moderation_actions.html b/bookwyrm/templates/user_admin/user_moderation_actions.html index e4460a68..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" %}
@@ -8,19 +8,38 @@

{% 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 %} +
+ {% 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/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py index 1b0b6008..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 @@ -134,6 +135,26 @@ 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) + + @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") diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index f80ef791..32ef4af0 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -163,6 +163,16 @@ 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+)/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 ca10189d..3f56e48f 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -34,7 +34,15 @@ 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, + moderator_delete_user, +) from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword from .search import Search 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 46c23884..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 @@ -77,12 +78,50 @@ 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) + + +@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) + + # 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) + # 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):