Merge branch 'main' into csv-import-failures

This commit is contained in:
Mouse Reeve 2021-09-11 09:34:38 -07:00
commit c0eded0003
9 changed files with 229 additions and 3 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/?$",

View file

@ -1,5 +1,6 @@
""" make sure all our nice views are available """
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

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)