Merge pull request #1402 from bookwyrm-social/admin

Adds admin dashboard
This commit is contained in:
Mouse Reeve 2021-09-11 07:40:39 -07:00 committed by GitHub
commit 8f0ea4cc7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 270 additions and 45 deletions

View file

@ -121,7 +121,7 @@
{% endif %}
{% if perms.bookwyrm.moderate_user %}
<li>
<a href="{% url 'settings-users' %}" class="navbar-item">
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
{% trans 'Admin' %}
</a>
</li>

View file

@ -0,0 +1,86 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% load humanize %}
{% load static %}
{% block title %}{% trans "Dashboard" %}{% endblock %}
{% block header %}{% trans "Dashboard" %}{% endblock %}
{% block panel %}
<div class="columns block has-text-centered">
<div class="column is-3">
<div class="notification">
<h3>{% trans "Total users" %}</h3>
<p class="title is-5">{{ users|intcomma }}</p>
</div>
</div>
<div class="column is-3">
<div class="notification">
<h3>{% trans "Active this month" %}</h3>
<p class="title is-5">{{ active_users|intcomma }}</p>
</div>
</div>
<div class="column is-3">
<div class="notification">
<h3>{% trans "Statuses" %}</h3>
<p class="title is-5">{{ statuses|intcomma }}</p>
</div>
</div>
<div class="column is-3">
<div class="notification">
<h3>{% trans "Works" %}</h3>
<p class="title is-5">{{ works|intcomma }}</p>
</div>
</div>
</div>
<div class="columns block is-multiline">
{% if reports %}
<div class="column">
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block">
{% blocktrans trimmed count counter=reports with display_count=reports|intcomma %}
{{ display_count }} open report
{% plural %}
{{ display_count }} open reports
{% endblocktrans %}
</a>
</div>
{% endif %}
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
<div class="column">
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-light">
{% blocktrans trimmed count counter=invite_requests with display_count=invite_requests|intcomma %}
{{ display_count }} invite request
{% plural %}
{{ display_count }} invite requests
{% endblocktrans %}
</a>
</div>
{% endif %}
</div>
<div class="block content">
<h2>{% trans "Instance Activity" %}</h2>
<div class="columns">
<div class="column">
<div class="box">
<canvas id="user_stats"></canvas>
</div>
</div>
<div class="column">
<div class="box">
<canvas id="status_stats"></canvas>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
{% include 'settings/dashboard_user_chart.html' %}
{% include 'settings/dashboard_status_chart.html' %}
{% endblock %}

View file

@ -0,0 +1,26 @@
{% load i18n %}
<script>
const status_labels = [{% for label in status_stats.labels %}"{{ label }}",{% endfor %}];
const status_data = {
labels: status_labels,
datasets: [{
label: '{% trans "Statuses posted" %}',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: {{ status_stats.total }},
}]
};
// === include 'setup' then 'config' above ===
const status_config = {
type: 'bar',
data: status_data,
options: {}
};
var statusStats = new Chart(
document.getElementById('status_stats'),
status_config
);
</script>

View file

@ -0,0 +1,29 @@
{% load i18n %}
<script>
const labels = [{% for label in user_stats.labels %}"{{ label }}",{% endfor %}];
const data = {
labels: labels,
datasets: [{
label: '{% trans "Total" %}',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: {{ user_stats.total }},
}, {
label: '{% trans "Active this month" %}',
backgroundColor: 'rgb(75, 192, 192)',
borderColor: 'rgb(75, 192, 192)',
data: {{ user_stats.active }},
}]
};
const config = {
type: 'line',
data: data,
options: {}
};
var userStats = new Chart(
document.getElementById('user_stats'),
config
);
</script>

View file

@ -18,6 +18,13 @@
<div class="block columns">
<nav class="menu column is-one-quarter">
<h2 class="menu-label">
{% url 'settings-dashboard' as url %}
<a
href="{{ url }}"
{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}
>{% trans "Dashboard" %}</a>
</h2>
{% if perms.bookwyrm.create_invites %}
<h2 class="menu-label">{% trans "Manage Users" %}</h2>
<ul class="menu-list">

View file

@ -75,7 +75,7 @@ class FederationViews(TestCase):
self.assertEqual(server.status, "federated")
view = views.federation.block_server
view = views.block_server
request = self.factory.post("")
request.user = self.local_user
request.user.is_superuser = True
@ -121,7 +121,7 @@ class FederationViews(TestCase):
request.user.is_superuser = True
with patch("bookwyrm.suggested_users.bulk_add_instance_task.delay") as mock:
views.federation.unblock_server(request, server.id)
views.unblock_server(request, server.id)
self.assertEqual(mock.call_count, 1)
self.assertEqual(mock.call_args[0][0], server.id)

View file

@ -65,6 +65,9 @@ urlpatterns = [
r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
),
# admin
re_path(
r"^settings/dashboard/?$", views.Dashboard.as_view(), name="settings-dashboard"
),
re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"),
re_path(
r"^settings/announcements/?$",
@ -83,7 +86,7 @@ urlpatterns = [
),
re_path(
r"^settings/email-preview/?$",
views.site.email_preview,
views.admin.site.email_preview,
name="settings-email-preview",
),
re_path(
@ -106,12 +109,12 @@ urlpatterns = [
),
re_path(
r"^settings/federation/(?P<server>\d+)/block?$",
views.federation.block_server,
views.block_server,
name="settings-federated-server-block",
),
re_path(
r"^settings/federation/(?P<server>\d+)/unblock?$",
views.federation.unblock_server,
views.unblock_server,
name="settings-federated-server-unblock",
),
re_path(

View file

@ -1,40 +1,13 @@
""" make sure all our nice views are available """
from .announcements import Announcements, Announcement, delete_announcement
from .author import Author, EditAuthor
from .block import Block, unblock
from .books import Book, EditBook, ConfirmEditBook
from .books import upload_cover, add_description, resolve_book
from .directory import Directory
from .discover import Discover
from .edit_user import EditUser, DeleteUser
from .editions import Editions, switch_edition
from .email_blocklist import EmailBlocklist
from .federation import Federation, FederatedServer
from .federation import AddFederatedServer, ImportServerBlocklist
from .federation import block_server, unblock_server
from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
from .goal import Goal, hide_goal
from .import_data import Import, ImportStatus
from .inbox import Inbox
from .interaction import Favorite, Unfavorite, Boost, Unboost
from .invite import ManageInvites, Invite, InviteRequest
from .invite import ManageInviteRequests, ignore_invite_request
from .isbn import Isbn
from .landing import About, Home, Landing
from .list import Lists, SavedLists, List, Curate, UserLists
from .list import save_list, unsave_list
from .list import delete_list
from .login import Login, Logout
from .notifications import Notifications
from .outbox import Outbox
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 (
from .admin.announcements import Announcements, Announcement, delete_announcement
from .admin.dashboard import Dashboard
from .admin.federation import Federation, FederatedServer
from .admin.federation import AddFederatedServer, ImportServerBlocklist
from .admin.federation import block_server, unblock_server
from .admin.email_blocklist import EmailBlocklist
from .admin.invite import ManageInvites, Invite, InviteRequest
from .admin.invite import ManageInviteRequests, ignore_invite_request
from .admin.reports import (
Report,
Reports,
make_report,
@ -43,15 +16,42 @@ from .reports import (
unsuspend_user,
moderator_delete_user,
)
from .admin.site import Site
from .admin.user_admin import UserAdmin, UserAdminList
from .author import Author, EditAuthor
from .block import Block, unblock
from .books import Book, EditBook, ConfirmEditBook
from .books import upload_cover, add_description, resolve_book
from .directory import Directory
from .discover import Discover
from .edit_user import EditUser, DeleteUser
from .editions import Editions, switch_edition
from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
from .goal import Goal, hide_goal
from .import_data import Import, ImportStatus
from .inbox import Inbox
from .interaction import Favorite, Unfavorite, Boost, Unboost
from .isbn import Isbn
from .landing import About, Home, Landing
from .list import Lists, SavedLists, List, Curate, UserLists
from .list import save_list, unsave_list, delete_list
from .login import Login, Logout
from .notifications import Notifications
from .outbox import Outbox
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 .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .search import Search
from .shelf import Shelf
from .shelf import create_shelf, delete_shelf
from .shelf import shelve, unshelve
from .site import Site
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .updates import get_notification_count, get_unread_status_count
from .user import User, Followers, Following, hide_suggestions
from .user_admin import UserAdmin, UserAdminList
from .wellknown import *

View file

@ -0,0 +1,74 @@
""" instance overview """
from datetime import timedelta
from django.contrib.auth.decorators import login_required, permission_required
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import models
# pylint: disable= no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.moderate_user", raise_exception=True),
name="dispatch",
)
class Dashboard(View):
"""admin overview"""
def get(self, request):
"""list of users"""
buckets = 6
bucket_size = 1 # days
now = timezone.now()
user_queryset = models.User.objects.filter(local=True, is_active=True)
user_stats = {"labels": [], "total": [], "active": []}
interval_end = now - timedelta(days=buckets * bucket_size)
while interval_end < timezone.now():
user_stats["total"].append(
user_queryset.filter(created_date__day__lte=interval_end.day).count()
)
user_stats["active"].append(
user_queryset.filter(
last_active_date__gt=interval_end - timedelta(days=31),
created_date__day__lte=interval_end.day,
).count()
)
user_stats["labels"].append(interval_end.strftime("%b %d"))
interval_end += timedelta(days=bucket_size)
status_queryset = models.Status.objects.filter(user__local=True, deleted=False)
status_stats = {"labels": [], "total": []}
interval_start = now - timedelta(days=buckets * bucket_size)
interval_end = interval_start + timedelta(days=bucket_size)
while interval_end < timezone.now():
status_stats["total"].append(
status_queryset.filter(
created_date__day__gt=interval_start.day,
created_date__day__lte=interval_end.day,
).count()
)
status_stats["labels"].append(interval_end.strftime("%b %d"))
interval_start = interval_end
interval_end += timedelta(days=bucket_size)
data = {
"users": user_queryset.count(),
"active_users": user_queryset.filter(
last_active_date__gte=now - timedelta(days=31)
).count(),
"statuses": status_queryset.count(),
"works": models.Work.objects.count(),
"reports": models.Report.objects.filter(resolved=False).count(),
"invite_requests": models.InviteRequest.objects.filter(
ignored=False, invite_sent=False
).count(),
"user_stats": user_stats,
"status_stats": status_stats,
}
return TemplateResponse(request, "settings/dashboard.html", data)

View file

@ -16,7 +16,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import emailing, forms, models
from bookwyrm.settings import PAGE_LENGTH
from . import helpers
from bookwyrm.views import helpers
# pylint: disable= no-self-use