Merge pull request #1390 from bookwyrm-social/better-suspension

Better suspension options for admins
This commit is contained in:
Mouse Reeve 2021-09-08 17:42:54 -07:00 committed by GitHub
commit 4e6670adab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 172 additions and 10 deletions

View file

@ -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,
),
),
]

View file

@ -13,6 +13,7 @@ DeactivationReason = models.TextChoices(
[
"pending",
"self_deletion",
"moderator_suspension",
"moderator_deletion",
"domain_block",
],

View file

@ -0,0 +1,20 @@
{% extends "components/inline_form.html" %}
{% load i18n %}
{% block header %}
{% trans "Permanently delete user" %}
{% endblock %}
{% block form %}
<form name="delete-user" action="{% url 'settings-delete-user' user.id %}" method="post">
{% csrf_token %}
<div class="field">
<label class="label" for="id_password">{% trans "Confirm password:" %}</label>
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required>
{% for error in form.password.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
</form>
{% endblock %}

View file

@ -1,6 +1,6 @@
{% load i18n %}
<div class="block content">
{% 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" %}
<div class="notification is-danger">
{% trans "Permanently deleted" %}
</div>
@ -8,19 +8,38 @@
<h3>{% trans "Actions" %}</h3>
<div class="is-flex">
{% if user.is_active %}
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p>
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}">
{% endif %}
{% if user.is_active or user.deactivation_reason == "pending" %}
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}" class="mr-1">
{% csrf_token %}
{% if user.is_active %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
{% else %}
<button class="button">{% trans "Un-suspend user" %}</button>
{% endif %}
</form>
{% else %}
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id %}" class="mr-1">
{% csrf_token %}
<button class="button">{% trans "Un-suspend user" %}</button>
</form>
{% endif %}
{% if user.local %}
<div>
{% 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" %}
</div>
{% endif %}
</div>
{% if user.local %}
<div>
{% include "user_admin/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %}
</div>
{% endif %}
{% if user.local %}
<div>
<form name="permission" method="post" action="{% url 'settings-user' user.id %}">

View file

@ -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")

View file

@ -153,6 +153,16 @@ urlpatterns = [
views.suspend_user,
name="settings-report-suspend",
),
re_path(
r"^settings/reports/(?P<user_id>\d+)/unsuspend/?$",
views.unsuspend_user,
name="settings-report-unsuspend",
),
re_path(
r"^settings/reports/(?P<user_id>\d+)/delete/?$",
views.moderator_delete_user,
name="settings-delete-user",
),
re_path(
r"^settings/reports/(?P<report_id>\d+)/resolve/?$",
views.resolve_report,

View file

@ -33,7 +33,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

View file

@ -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"]):

View file

@ -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):