Merge pull request #966 from bookwyrm-social/user-admin

User admin
This commit is contained in:
Mouse Reeve 2021-04-20 13:43:51 -07:00 committed by GitHub
commit 2e12d54687
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 221 additions and 99 deletions

View file

@ -150,6 +150,12 @@ class LimitedEditUserForm(CustomForm):
help_texts = {f: None for f in fields}
class UserGroupForm(CustomForm):
class Meta:
model = models.User
fields = ["groups"]
class TagForm(CustomForm):
class Meta:
model = models.Tag

View file

@ -15,76 +15,9 @@
{% include 'moderation/report_preview.html' with report=report %}
</div>
<div class="block columns">
<div class="column is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "User details" %}</h4>
<div class="box is-flex-grow-1">
{% include 'user/user_preview.html' with user=report.user %}
{% if report.user.summary %}
<div class="box content has-background-white-ter is-shadowless">
{{ report.user.summary | to_markdown | safe }}
</div>
{% endif %}
{% include 'user_admin/user_info.html' with user=report.user %}
<p class="mt-2"><a href="{{ report.user.local_path }}">{% trans "View user profile" %}</a></p>
</div>
</div>
{% if not report.user.local %}
{% with server=report.user.federated_server %}
<div class="column is-half is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "Instance details" %}</h4>
<div class="box content is-flex-grow-1">
{% if server %}
<h5>{{ server.server_name }}</h5>
<dl>
<div class="is-flex">
<dt>{% trans "Software:" %}</dt>
<dd>{{ server.application_type }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Version:" %}</dt>
<dd>{{ server.application_version }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Status:" %}</dt>
<dd>{{ server.status }}</dd>
</div>
</dl>
{% if server.notes %}
<h5>{% trans "Notes" %}</h5>
<div class="box content has-background-white-ter is-shadowless">
{{ server.notes }}
</div>
{% endif %}
<p>
<a href="{% url 'settings-federated-server' server.id %}">{% trans "View instance" %}</a>
</p>
{% else %}
<em>{% trans "Not set" %}</em>
{% endif %}
</div>
</div>
{% endwith %}
{% endif %}
</div>
<div class="block content">
<h3>{% trans "Actions" %}</h3>
<div class="is-flex">
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' report.user.username %}">{% trans "Send direct message" %}</a>
</p>
<form name="deactivate" method="post" action="{% url 'settings-report-deactivate' report.id %}">
{% csrf_token %}
{% if report.user.is_active %}
<button type="submit" class="button is-danger is-light">{% trans "Deactivate user" %}</button>
{% else %}
<button class="button">{% trans "Reactivate user" %}</button>
{% endif %}
</form>
</div>
</div>
{% include 'user_admin/user_moderation_actions.html' with user=report.user %}
<div class="block">
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>

View file

@ -30,7 +30,7 @@
</ul>
</div>
{% include 'settings/user_admin_filters.html' %}
{% include 'user_admin/user_admin_filters.html' %}
<div class="block">
{% if not reports %}

View file

@ -0,0 +1,19 @@
{% extends 'settings/admin_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{{ user.username }}{% endblock %}
{% block header %}{{ user.username }}{% endblock %}
{% block panel %}
<div class="block">
<a href="{% url 'settings-users' %}">{% trans "Back to users" %}</a>
</div>
{% include 'user_admin/user_info.html' with user=user %}
{% include 'user_admin/user_moderation_actions.html' with user=user %}
{% endblock %}

View file

@ -13,7 +13,7 @@
{% block panel %}
{% include 'settings/user_admin_filters.html' %}
{% include 'user_admin/user_admin_filters.html' %}
<table class="table is-striped">
<tr>
@ -41,7 +41,7 @@
</tr>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td><a href="{% url 'settings-user' user.id %}">{{ user.username }}</a></td>
<td>{{ user.created_date }}</td>
<td>{{ user.last_active_date }}</td>
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>

View file

@ -1,6 +1,6 @@
{% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %}
{% include 'settings/server_filter.html' %}
{% include 'settings/username_filter.html' %}
{% include 'user_admin/server_filter.html' %}
{% include 'user_admin/username_filter.html' %}
{% endblock %}

View file

@ -0,0 +1,56 @@
{% load i18n %}
{% load bookwyrm_tags %}
<div class="block columns">
<div class="column is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "User details" %}</h4>
<div class="box is-flex-grow-1">
{% include 'user/user_preview.html' with user=user %}
{% if user.summary %}
<div class="box content has-background-white-ter is-shadowless">
{{ user.summary | to_markdown | safe }}
</div>
{% endif %}
<p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p>
</div>
</div>
{% if not user.local %}
{% with server=user.federated_server %}
<div class="column is-half is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "Instance details" %}</h4>
<div class="box content is-flex-grow-1">
{% if server %}
<h5>{{ server.server_name }}</h5>
<dl>
<div class="is-flex">
<dt>{% trans "Software:" %}</dt>
<dd>{{ server.application_type }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Version:" %}</dt>
<dd>{{ server.application_version }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Status:" %}</dt>
<dd>{{ server.status }}</dd>
</div>
</dl>
{% if server.notes %}
<h5>{% trans "Notes" %}</h5>
<div class="box content has-background-white-ter is-shadowless">
{{ server.notes }}
</div>
{% endif %}
<p>
<a href="{% url 'settings-federated-server' server.id %}">{% trans "View instance" %}</a>
</p>
{% else %}
<em>{% trans "Not set" %}</em>
{% endif %}
</div>
</div>
{% endwith %}
{% endif %}
</div>

View file

@ -0,0 +1,42 @@
{% load i18n %}
<div class="block content">
<h3>{% trans "Actions" %}</h3>
<div class="is-flex">
<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 %}">
{% 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>
</div>
{% if user.local %}
<div>
<form name="permission" method="post" action="{% url 'settings-user' user.id %}">
{% csrf_token %}
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
{% if group_form.non_field_errors %}
{{ group_form.non_field_errors }}
{% endif %}
{% with group=user.groups.first %}
<div class="select">
<select name="groups" id="id_user_group">
{% for value, name in group_form.fields.groups.choices %}
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>{{ name|title }}</option>
{% endfor %}
<option value="" {% if not group %}selected{% endif %}>User</option>
</select>
</div>
{% for error in group_form.groups.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
{% endwith %}
<button class="button">{% trans "Save" %}</button>
</form>
</div>
{% endif %}
</div>

View file

@ -1,5 +1,4 @@
""" test for app action functionality """
from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
@ -115,22 +114,19 @@ class ReportViews(TestCase):
report.refresh_from_db()
self.assertFalse(report.resolved)
def test_deactivate_user(self):
def test_suspend_user(self):
""" toggle whether a user is able to log in """
self.assertTrue(self.rat.is_active)
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
request = self.factory.post("")
request.user = self.local_user
request.user.is_superuser = True
# de-activate
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.deactivate_user(request, report.id)
views.suspend_user(request, self.rat.id)
self.rat.refresh_from_db()
self.assertFalse(self.rat.is_active)
# re-activate
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.deactivate_user(request, report.id)
views.suspend_user(request, self.rat.id)
self.rat.refresh_from_db()
self.assertTrue(self.rat.is_active)

View file

@ -1,4 +1,6 @@
""" test for app action functionality """
from unittest.mock import patch
from django.contrib.auth.models import Group
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
@ -21,9 +23,9 @@ class UserAdminViews(TestCase):
)
models.SiteSettings.objects.create()
def test_user_admin_page(self):
def test_user_admin_list_page(self):
""" there are so many views, this just makes sure it LOADS """
view = views.UserAdmin.as_view()
view = views.UserAdminList.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
@ -31,3 +33,38 @@ class UserAdminViews(TestCase):
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
def test_user_admin_page(self):
""" there are so many views, this just makes sure it LOADS """
view = views.UserAdmin.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
def test_user_admin_page_post(self):
""" set the user's group """
group = Group.objects.create(name="editor")
self.assertEqual(
list(self.local_user.groups.values_list("name", flat=True)), []
)
view = views.UserAdmin.as_view()
request = self.factory.post("", {"groups": [group.id]})
request.user = self.local_user
request.user.is_superuser = True
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(
list(self.local_user.groups.values_list("name", flat=True)), ["editor"]
)

View file

@ -51,13 +51,20 @@ urlpatterns = [
r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
),
# admin
re_path(r"^settings/site-settings", views.Site.as_view(), name="settings-site"),
re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"),
re_path(
r"^settings/email-preview",
r"^settings/email-preview/?$",
views.site.email_preview,
name="settings-email-preview",
),
re_path(r"^settings/users", views.UserAdmin.as_view(), name="settings-users"),
re_path(
r"^settings/users/?$", views.UserAdminList.as_view(), name="settings-users"
),
re_path(
r"^settings/users/(?P<user>\d+)/?$",
views.UserAdmin.as_view(),
name="settings-user",
),
re_path(
r"^settings/federation/?$",
views.Federation.as_view(),
@ -113,9 +120,9 @@ urlpatterns = [
name="settings-report",
),
re_path(
r"^settings/reports/(?P<report_id>\d+)/deactivate/?$",
views.deactivate_user,
name="settings-report-deactivate",
r"^settings/reports/(?P<user_id>\d+)/suspend/?$",
views.suspend_user,
name="settings-report-suspend",
),
re_path(
r"^settings/reports/(?P<report_id>\d+)/resolve/?$",

View file

@ -25,7 +25,7 @@ from .notifications import Notifications
from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough, delete_readthrough
from .reading import start_reading, finish_reading, delete_progressupdate
from .reports import Report, Reports, make_report, resolve_report, deactivate_user
from .reports import Report, Reports, make_report, resolve_report, suspend_user
from .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .search import Search
@ -37,5 +37,5 @@ from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .tag import Tag, AddTag, RemoveTag
from .updates import get_notification_count, get_unread_status_count
from .user import User, EditUser, Followers, Following
from .user_admin import UserAdmin
from .user_admin import UserAdmin, UserAdminList
from .wellknown import *

View file

@ -74,12 +74,13 @@ class Report(View):
@login_required
@permission_required("bookwyrm_moderate_user")
def deactivate_user(_, report_id):
def suspend_user(_, user_id):
""" mark an account as inactive """
report = get_object_or_404(models.Report, id=report_id)
report.user.is_active = not report.user.is_active
report.user.save()
return redirect("settings-report", report.id)
user = get_object_or_404(models.User, id=user_id)
user.is_active = not user.is_active
# 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

View file

@ -1,11 +1,12 @@
""" manage user """
from django.contrib.auth.decorators import login_required, permission_required
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import models
from bookwyrm import forms, models
from bookwyrm.settings import PAGE_LENGTH
@ -15,7 +16,7 @@ from bookwyrm.settings import PAGE_LENGTH
permission_required("bookwyrm.moderate_users", raise_exception=True),
name="dispatch",
)
class UserAdmin(View):
class UserAdminList(View):
""" admin view of users on this server """
def get(self, request):
@ -49,4 +50,28 @@ class UserAdmin(View):
"sort": sort,
"server": server,
}
return TemplateResponse(request, "settings/user_admin.html", data)
return TemplateResponse(request, "user_admin/user_admin.html", data)
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.moderate_users", raise_exception=True),
name="dispatch",
)
class UserAdmin(View):
""" moderate an individual user """
def get(self, request, user):
""" user view """
user = get_object_or_404(models.User, id=user)
data = {"user": user, "group_form": forms.UserGroupForm()}
return TemplateResponse(request, "user_admin/user.html", data)
def post(self, request, user):
""" update user group """
user = get_object_or_404(models.User, id=user)
form = forms.UserGroupForm(request.POST, instance=user)
if form.is_valid():
form.save()
data = {"user": user, "group_form": form}
return TemplateResponse(request, "user_admin/user.html", data)