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", "pending",
"self_deletion", "self_deletion",
"moderator_suspension",
"moderator_deletion", "moderator_deletion",
"domain_block", "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 %} {% load i18n %}
<div class="block content"> <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"> <div class="notification is-danger">
{% trans "Permanently deleted" %} {% trans "Permanently deleted" %}
</div> </div>
@ -8,19 +8,38 @@
<h3>{% trans "Actions" %}</h3> <h3>{% trans "Actions" %}</h3>
<div class="is-flex"> <div class="is-flex">
{% if user.is_active %}
<p class="mr-1"> <p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a> <a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p> </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 %} {% csrf_token %}
{% if user.is_active %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button> <button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
{% else %}
<button class="button">{% trans "Un-suspend user" %}</button>
{% endif %}
</form> </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> </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 %} {% if user.local %}
<div> <div>
<form name="permission" method="post" action="{% url 'settings-user' user.id %}"> <form name="permission" method="post" action="{% url 'settings-user' user.id %}">

View file

@ -1,4 +1,5 @@
""" test for app action functionality """ """ test for app action functionality """
import json
from unittest.mock import patch from unittest.mock import patch
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
@ -134,6 +135,26 @@ class ReportViews(TestCase):
self.assertFalse(self.rat.is_active) self.assertFalse(self.rat.is_active)
# re-activate # re-activate
views.suspend_user(request, self.rat.id) views.unsuspend_user(request, self.rat.id)
self.rat.refresh_from_db() self.rat.refresh_from_db()
self.assertTrue(self.rat.is_active) 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, views.suspend_user,
name="settings-report-suspend", 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( re_path(
r"^settings/reports/(?P<report_id>\d+)/resolve/?$", r"^settings/reports/(?P<report_id>\d+)/resolve/?$",
views.resolve_report, 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 delete_readthrough, delete_progressupdate
from .reading import ReadingStatus from .reading import ReadingStatus
from .register import Register, ConfirmEmail, ConfirmEmailCode, resend_link 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 .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .search import Search from .search import Search

View file

@ -55,7 +55,6 @@ class DeleteUser(View):
def post(self, request): def post(self, request):
"""les get fancy with images""" """les get fancy with images"""
form = forms.DeleteUserForm(request.POST, instance=request.user) 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 # idk why but I couldn't get check_password to work on request.user
user = models.User.objects.get(id=request.user.id) user = models.User.objects.get(id=request.user.id)
if form.is_valid() and user.check_password(form.cleaned_data["password"]): if form.is_valid() and user.check_password(form.cleaned_data["password"]):

View file

@ -1,5 +1,6 @@
""" moderation via flagged posts and users """ """ moderation via flagged posts and users """
from django.contrib.auth.decorators import login_required, permission_required 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.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -77,12 +78,50 @@ class Report(View):
def suspend_user(_, user_id): def suspend_user(_, user_id):
"""mark an account as inactive""" """mark an account as inactive"""
user = get_object_or_404(models.User, id=user_id) 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 # this isn't a full deletion, so we don't want to tell the world
user.save(broadcast=False) user.save(broadcast=False)
return redirect("settings-user", user.id) 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 @login_required
@permission_required("bookwyrm_moderate_post") @permission_required("bookwyrm_moderate_post")
def resolve_report(_, report_id): def resolve_report(_, report_id):