From 004435920255a9e4e76f076be0a0c01b07eee660 Mon Sep 17 00:00:00 2001 From: catborise Date: Sat, 31 Oct 2026 13:26:31 +0300 Subject: [PATCH] feat(accounts): the administrator's view of a user as an identity strip and cards A superuser's page of a user was a heading, two unlabelled icon buttons and three tabs: the VMs a table of check columns, the SSH keys a table that pushed the page sideways on a phone. Now a card at the top shows the user's name, role, Blocked when they are, username, email, groups, last login and joining date, with Edit user and Email OTP QR code as labelled buttons and links to the sections below with their counts. The VMs card shows each VM with its host and what the user may do beyond power and console as badges in the VM Users tab's words (Change settings, Delete the VM, Change console settings), with Change access and Remove from user in a menu, and Add VM in its header. The SSH keys and the API tokens are cards; the key rows are now one include with the user's own Profile, and both lists say when they are empty. The full key shows over its fingerprint. The tab script had no page left and goes. --- accounts/templates/account.html | 192 +++++++++++-------------- accounts/templates/api_token_list.html | 2 + accounts/templates/profile.html | 19 +-- accounts/templates/ssh_key_list.html | 21 +++ accounts/templates/tab_from_hash.html | 7 - accounts/tests/test_account_page.py | 24 +++- accounts/views.py | 2 +- 7 files changed, 131 insertions(+), 136 deletions(-) create mode 100644 accounts/templates/ssh_key_list.html delete mode 100644 accounts/templates/tab_from_hash.html diff --git a/accounts/templates/account.html b/accounts/templates/account.html index e56d7259..0f58d8d3 100644 --- a/accounts/templates/account.html +++ b/accounts/templates/account.html @@ -4,118 +4,94 @@ {% load bootstrap_icons %} {% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %} -{% block page_heading %}{% trans "User Profile" %}: {{ user }}{% endblock page_heading %} - -{% block page_heading_extra %} -{% if otp_enabled %} -
{% csrf_token %} - -
-{% endif %} - - {% bs_icon 'pencil' %} - - - {% bs_icon 'plus-circle-fill' %} - -{% endblock page_heading_extra %} +{% block page_heading %}{% trans "User Profile" %}{% endblock page_heading %} {% block content %} - - -
-
-
- - - - - - - - - - - - - {% for inst in user_insts %} - - - - - - - - - - {% empty %} - - {% endfor %} - -
#{% trans "Instance" %}{% trans "VNC" %}{% trans "Resize" %}{% trans "Delete" %}{% trans "Action" %}
{{ forloop.counter }}{{ inst.instance.name }}{% if inst.is_vnc %}{% bs_icon 'check' %}{% endif %}{% if inst.is_change %}{% bs_icon 'check' %}{% endif %}{% if inst.is_delete %}{% bs_icon 'check' %}{% endif %} - - {% bs_icon 'pencil' %} - - - - {% bs_icon 'trash' %} - -
{% trans "No VMs." %}
+
+
+
+
+
+

{{ user.get_full_name|default:user.username }}

+ {% if user.is_superuser %} + {% trans "Superuser" %} + {% elif user.is_staff %} + {% trans "Template editor" %} + {% endif %} + {% if not user.is_active %}{% trans "Blocked" %}{% endif %} +
+
+ {{ user.username }} + {% if user.email %}{{ user.email }}{% endif %} + {% for group in user.groups.all %}{{ group.name }}{% endfor %} +
+
+ {% trans "Last login" %} {{ user.last_login|date:"Y-m-d H:i"|default:_("never") }} + {% trans "Joined" %} {{ user.date_joined|date:"Y-m-d" }} +
-
-
-
- - - - - - - - - - {% for publickey in publickeys %} - - - - - - {% endfor %} - -
{% trans "Key name" %}{% trans "Public key" %}{% trans "Action" %}
{{ publickey.keyname }}{{ publickey.keypublic|truncatechars:64 }} - - {% bs_icon 'trash' %} - -
+
+ {% bs_icon 'pencil' %} {% trans "Edit user" %} + {% if otp_enabled %} +
{% csrf_token %} + +
+ {% endif %}
-
- {% if api_tokens %} -
{% include "api_token_list.html" with owner=user %}
- {% else %} -

{% trans "No API tokens." %}

- {% endif %} -
+ +
+ +
+
+

{% trans "VMs" %} {{ user_insts|length }}

+ {% bs_icon 'plus-lg' %} {% trans "Add VM" %} +
+
+ {% for inst in user_insts %} +
+
+
+
+ {{ inst.instance.name }} +
{% trans "Host" %}: {{ inst.instance.compute.name }}
+
+
+ {# the rights as the VM's Users tab names them: every owner powers the VM and opens its console #} + {% if inst.is_change %}{% trans "Change settings" %}{% endif %} + {% if inst.is_delete %}{% trans "Delete the VM" %}{% endif %} + {% if inst.is_change and inst.is_vnc %}{% trans "Change console settings" %}{% endif %} + {% if not inst.is_change and not inst.is_delete %}{% trans "Power and console only" %}{% endif %} +
+
+ +
+
+ {% empty %} +
{% trans "No VMs." %}
+ {% endfor %} +
+
+ +
+

{% trans "SSH Keys" %} {{ publickeys|length }}

+
{% include "ssh_key_list.html" with owner=user %}
+
+ +
+

{% trans "API Tokens" %} {{ api_tokens|length }}

+
{% include "api_token_list.html" with owner=user %}
+
{% endblock content %} -{% block script %} -{% include "tab_from_hash.html" %} -{% endblock %} diff --git a/accounts/templates/api_token_list.html b/accounts/templates/api_token_list.html index 994d6494..41d47a55 100644 --- a/accounts/templates/api_token_list.html +++ b/accounts/templates/api_token_list.html @@ -20,4 +20,6 @@
{% trans "Last used" %} {% if token.last_used %}{{ token.last_used|date:"Y-m-d H:i" }}{% else %}{% trans "never" %}{% endif %}
+{% empty %} +
{% trans "No API tokens." %}
{% endfor %} diff --git a/accounts/templates/profile.html b/accounts/templates/profile.html index af08750d..22a58a33 100644 --- a/accounts/templates/profile.html +++ b/accounts/templates/profile.html @@ -2,7 +2,6 @@ {% load i18n %} {% load django_bootstrap5 %} {% load bootstrap_icons %} -{% load tags_fingerprint %} {% block title %}{% trans "Profile" %}{% endblock %} @@ -48,23 +47,7 @@
{% trans "Offered when you create a VM and on a VM's Access tab." %}
-
- {% for key in publickeys %} -
-
- {{ key.keyname }} - -
-
{% ssh_to_fingerprint key.keypublic %}
-
- {% endfor %} -
+
{% include "ssh_key_list.html" %}
{% csrf_token %} diff --git a/accounts/templates/ssh_key_list.html b/accounts/templates/ssh_key_list.html new file mode 100644 index 00000000..ad503768 --- /dev/null +++ b/accounts/templates/ssh_key_list.html @@ -0,0 +1,21 @@ +{% load i18n %} +{% load bootstrap_icons %} +{% load tags_fingerprint %} +{# the keys of the signed-in user, or of owner on a superuser's view of a user: the items of a list-group #} +{% for key in publickeys %} +
+
+ {{ key.keyname }} + +
+
{% ssh_to_fingerprint key.keypublic %}
+
+{% empty %} +
{% trans "No SSH keys." %}
+{% endfor %} diff --git a/accounts/templates/tab_from_hash.html b/accounts/templates/tab_from_hash.html deleted file mode 100644 index b418e8a5..00000000 --- a/accounts/templates/tab_from_hash.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/accounts/tests/test_account_page.py b/accounts/tests/test_account_page.py index 039179c0..a94306c3 100644 --- a/accounts/tests/test_account_page.py +++ b/accounts/tests/test_account_page.py @@ -30,9 +30,29 @@ class AccountPageTests(TestCase): self.assertContains(response, "ap-vm") self.assertNotContains(response, "True", html=True) self.assertNotContains(response, "False", html=True) + # what the user may do beyond power and console, in the VM Users tab's words, and the VM's host + self.assertContains(response, "Power and console only") + self.assertNotContains(response, "Change settings") + self.assertContains(response, "Host: ap-host") + # the console settings come with the right to change the VM + UserInstance.objects.filter(user=self.alice).update(is_change=True) + response = self.page() + self.assertContains(response, "Change settings") + self.assertContains(response, "Change console settings") + self.assertNotContains(response, "Power and console only") - def test_a_user_without_vms_is_told_so(self): - self.assertContains(self.page(), "No VMs.") + def test_a_blocked_user_is_marked(self): + self.assertNotContains(self.page(), "Blocked") + self.alice.is_active = False + self.alice.save() + self.assertContains(self.page(), 'Blocked', html=True) + + def test_a_user_without_vms_or_tokens_is_told_so(self): + response = self.page() + self.assertContains(response, "No VMs.") + self.assertContains(response, "No API tokens.") + # the whole key shows over its fingerprint + self.assertContains(response, f'title="{KEY}"') def test_a_superuser_deletes_a_users_key(self): url = reverse("accounts:account_ssh_key_delete", args=[self.alice.pk, self.key.pk]) diff --git a/accounts/views.py b/accounts/views.py index ef75f062..d29571bb 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -169,7 +169,7 @@ def ssh_key_delete(request, pk, user_id=None): @superuser_only def account(request, user_id): user = get_object_or_404(User, id=user_id) - user_insts = UserInstance.objects.filter(user_id=user_id).select_related("instance") + user_insts = UserInstance.objects.filter(user_id=user_id).select_related("instance__compute") publickeys = UserSSHKey.objects.filter(user_id=user_id) return render(