1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

feat(admin): the user list shows how many API tokens each user has

Which users can reach the API with a token was seen only by opening each
user. The user list now has an API tokens column: the number of the
user's tokens that still work (expired ones are left out; they are
deleted only at the user's next token), linked to the user's token tab.
One query counts them for all users.
This commit is contained in:
catborise 2026-10-25 20:38:05 +03:00
parent f004552148
commit 8063dcec66
3 changed files with 23 additions and 1 deletions

View file

@ -12,6 +12,7 @@ from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from logs.models import Logs
from lxml import html
class ApiTokenTests(TestCase):
@ -176,3 +177,18 @@ class AdminApiTokenTests(TestCase):
self.client.force_login(self.admin)
self.assertEqual(self.client.post(self.delete_url(other)).status_code, 404)
self.assertTrue(ApiToken.objects.exists())
def test_the_user_list_counts_each_users_live_tokens(self):
self.client.force_login(self.user)
self.client.post(reverse("accounts:api_token_create"), {"name": "old", "days": "30"})
ApiToken.objects.filter(name="old").update(expiry=timezone.now() - timedelta(days=1))
self.client.force_login(self.admin)
page = html.fromstring(self.client.get(reverse("admin:user_list")).content)
headers = [th.text_content().strip() for th in page.xpath("//thead//th")]
column = headers.index("API tokens")
cells = {row.xpath("td")[0].text_content().strip(): row.xpath("td")[column] for row in page.xpath("//tbody/tr")}
# the expired one is not counted; the count opens the user's tokens
self.assertEqual(cells["tok-user"].text_content().strip(), "1")
self.assertEqual(cells["tok-user"].xpath(".//a/@href"),
[reverse("accounts:account", args=[self.user.id]) + "#apitokens"])
self.assertEqual(cells["tok-admin"].text_content().strip(), "")

View file

@ -35,6 +35,7 @@
<th span="col">{% trans "Staff" %}</th>
<th span="col">{% trans "Superuser" %}</th>
<th span="col">{% trans "Can Clone" %}</th>
<th span="col">{% trans "API tokens" %}</th>
<th span="col">{% trans "Actions" %}</th>
</tr>
</thead>
@ -56,6 +57,7 @@
<td>{% if user.is_staff %}{% bs_icon 'check' %}{% endif %}</td>
<td>{% if user.is_superuser %}{% bs_icon 'check' %}{% endif %}</td>
<td>{% if can_clone %}{% bs_icon 'check' %}{% endif %}</td>
<td>{% if user.api_tokens %}<a href="{% url 'accounts:account' user.id %}#apitokens">{{ user.api_tokens }}</a>{% endif %}</td>
<td>
<div class="float-end btn-group">
<a class="btn btn-success" title="{%trans "View Profile" %}" href="{% url 'accounts:account' user.id %}">{% bs_icon 'eye-fill' %}</a>

View file

@ -9,7 +9,9 @@ from django.contrib.auth.forms import AdminPasswordChangeForm
from django.contrib.auth.models import Group, User
from django.core.paginator import Paginator
from django.db import transaction
from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST
@ -96,7 +98,9 @@ def group_delete(request, pk):
@superuser_only
def user_list(request):
users = User.objects.all()
# API tokens that still work: an expired one is deleted only at the user's next create
live = Q(auth_token_set__expiry__isnull=True) | Q(auth_token_set__expiry__gt=timezone.now())
users = User.objects.annotate(api_tokens=Count("auth_token_set", filter=live))
return render(
request,
"admin/user_list.html",