mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 20:26:48 +00:00
style(accounts): the password pages have the account forms' layout
The page where a user changes their own password and the one where an administrator sets a user's password shared an old card: a horizontal form, a blue Cancel and a green Change. The administrator's had Django's "Password-based authentication: Enabled/Disabled" with a paragraph about other backends, and its password fields stayed while Disabled was chosen. Both pages now have the user and group forms' layout, each its own template. The administrator chooses how the user signs in, With a password or Without a password (such as LDAP), and the password fields and "must change it" show only with a password; Cancel goes back to the user. A user's own page says that their other sessions end, or, when they must change it, that they choose one before going on, without a Cancel. A refused password is said at its field only, without the general "Wrong Data Provided". The section styles of these forms move into webvirtcloud.css.
This commit is contained in:
parent
61979be731
commit
84dfbd5884
14 changed files with 323 additions and 193 deletions
|
|
@ -232,6 +232,8 @@ class AdminPasswordForm(AdminPasswordChangeForm):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["must_change_password"] = self.fields.pop("must_change_password") # last, after Django's own
|
||||
# Django's "Password-based authentication: Enabled/Disabled", said as how the user signs in
|
||||
self.fields["usable_password"].choices = [("true", _("With a password")), ("false", _("Without a password"))]
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
|
|
|
|||
|
|
@ -7,39 +7,30 @@
|
|||
|
||||
{% block page_heading %}{{ title }}{% endblock page_heading %}
|
||||
|
||||
{% block style %}
|
||||
<style>
|
||||
/* as the user form: what a section is about on the left, its fields on the right */
|
||||
.group-form-section { border-top: 1px solid var(--bs-border-color); padding: 1.5rem 0; }
|
||||
.group-form-section:first-of-type { border-top: 0; padding-top: .5rem; }
|
||||
.group-form-fields { max-width: 52rem; }
|
||||
</style>
|
||||
{% endblock style %}
|
||||
|
||||
{% block content %}
|
||||
<form id="create-update" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form type="non_fields" %}
|
||||
|
||||
<section class="group-form-section">
|
||||
<section class="form-section">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-3">
|
||||
<h2 class="h6 fw-semibold mb-1">{% trans "Group" %}</h2>
|
||||
<p class="small text-body-secondary mb-0">{% trans "Users who share a set of permissions." %}</p>
|
||||
</div>
|
||||
<div class="col-lg-9 group-form-fields">
|
||||
<div class="col-lg-9 form-section-fields">
|
||||
<div class="row"><div class="col-sm-6">{% bootstrap_field form.name %}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="group-form-section">
|
||||
<section class="form-section">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-3">
|
||||
<h2 class="h6 fw-semibold mb-1">{% trans "Permissions" %}</h2>
|
||||
<p class="small text-body-secondary mb-0">{% trans "Each member has these permissions besides their own." %}</p>
|
||||
</div>
|
||||
<div class="col-lg-9 group-form-fields">
|
||||
<div class="col-lg-9 form-section-fields">
|
||||
<div class="border-top">
|
||||
{% for box, help in form.permission_rows %}
|
||||
<div class="form-check form-switch border-bottom py-2 ps-5">
|
||||
|
|
@ -57,13 +48,13 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="group-form-section">
|
||||
<section class="form-section">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-3">
|
||||
<h2 class="h6 fw-semibold mb-1">{% trans "Members" %}</h2>
|
||||
<p class="small text-body-secondary mb-0">{% trans "Who gets these permissions." %}</p>
|
||||
</div>
|
||||
<fieldset class="col-lg-9 group-form-fields">
|
||||
<fieldset class="col-lg-9 form-section-fields">
|
||||
<legend class="visually-hidden">{% trans "Members" %}</legend>
|
||||
<input id="filter" class="form-control form-control-sm mb-2" style="max-width: 20rem" type="search" placeholder="{% trans "Search" %}" aria-label="{% trans "Search users" %}" data-forget>
|
||||
{# the members first, then everyone else; each by username #}
|
||||
|
|
@ -77,7 +68,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<div class="group-form-section d-flex justify-content-end gap-2">
|
||||
<div class="form-section d-flex justify-content-end gap-2">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'admin:group_list' %}">{% trans "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-primary">{% if group %}{% trans "Save changes" %}{% else %}{% trans "Create group" %}{% endif %}</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,23 +7,13 @@
|
|||
|
||||
{% block page_heading %}{{ title }}{% endblock page_heading %}
|
||||
|
||||
{% block style %}
|
||||
<style>
|
||||
/* each section: what it is about on the left, its fields on the right */
|
||||
.user-form-section { border-top: 1px solid var(--bs-border-color); padding: 1.5rem 0; }
|
||||
.user-form-section:first-of-type { border-top: 0; padding-top: .5rem; }
|
||||
.user-form-fields { max-width: 52rem; }
|
||||
.user-form-role:has(input:checked) { border-color: var(--bs-primary) !important; box-shadow: inset 0 0 0 1px var(--bs-primary); }
|
||||
</style>
|
||||
{% endblock style %}
|
||||
|
||||
{% block content %}
|
||||
{# who the user is, what they manage, what they may use, then what more they may do #}
|
||||
<form id="create-update" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors user_form type="non_fields" %}
|
||||
|
||||
<section class="user-form-section">
|
||||
<section class="form-section">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-3">
|
||||
<h2 class="h6 fw-semibold mb-1">{% trans "Account" %}</h2>
|
||||
|
|
@ -33,7 +23,7 @@
|
|||
<p class="small text-body-secondary mb-0">{% trans "What the user signs in with." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg-9 user-form-fields">
|
||||
<div class="col-lg-9 form-section-fields">
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">{% bootstrap_field user_form.username %}</div>
|
||||
<div class="col-sm-6">
|
||||
|
|
@ -61,20 +51,20 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="user-form-section">
|
||||
<section class="form-section">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-3">
|
||||
<h2 class="h6 fw-semibold mb-1" id="role-heading">{% trans "Role" %}</h2>
|
||||
<p class="small text-body-secondary mb-0">{% trans "What the user manages." %}</p>
|
||||
</div>
|
||||
<div class="col-lg-9 user-form-fields">
|
||||
<div class="col-lg-9 form-section-fields">
|
||||
{% trans "Uses the VMs given to them, within their quota." as user_help %}
|
||||
{% trans "May change and delete the template VMs given to them." as editor_help %}
|
||||
{% trans "Manages the panel: every permission, no quota." as superuser_help %}
|
||||
<div class="row row-cols-1 row-cols-md-3 g-2" role="radiogroup" aria-labelledby="role-heading">
|
||||
{% for choice in user_form.role %}
|
||||
<div class="col">
|
||||
<label class="user-form-role border rounded p-3 d-flex gap-2 h-100">
|
||||
<label class="form-choice border rounded p-3 d-flex gap-2 h-100">
|
||||
<input class="form-check-input flex-shrink-0 mt-1" type="radio" name="role" value="{{ choice.data.value }}"{% if choice.data.selected %} checked{% endif %}>
|
||||
<span><span class="d-block fw-semibold">{{ choice.choice_label }}</span>
|
||||
<span class="small text-body-secondary">{% if choice.data.value == "superuser" %}{{ superuser_help }}{% elif choice.data.value == "template_editor" %}{{ editor_help }}{% else %}{{ user_help }}{% endif %}</span></span>
|
||||
|
|
@ -89,17 +79,17 @@
|
|||
|
||||
{# user_form.js hides the sections a superuser does not need while Superuser is chosen #}
|
||||
{% with superuser=user_form.role.value %}
|
||||
<p id="superuser-note" class="user-form-section small text-body-secondary mb-0"{% if superuser != "superuser" %} hidden{% endif %}>
|
||||
<p id="superuser-note" class="form-section small text-body-secondary mb-0"{% if superuser != "superuser" %} hidden{% endif %}>
|
||||
{% trans "A superuser has every permission and no quota: there is nothing more to set." %}
|
||||
</p>
|
||||
|
||||
<section class="user-form-section" data-not-superuser{% if superuser == "superuser" %} hidden{% endif %}>
|
||||
<section class="form-section" data-not-superuser{% if superuser == "superuser" %} hidden{% endif %}>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-3">
|
||||
<h2 class="h6 fw-semibold mb-1">{% trans "Quota" %}</h2>
|
||||
<p class="small text-body-secondary mb-0">{% trans "The most the user's VMs may have in total." %}</p>
|
||||
</div>
|
||||
<div class="col-lg-9 user-form-fields">
|
||||
<div class="col-lg-9 form-section-fields">
|
||||
<div class="row row-cols-2 row-cols-md-4 g-3">
|
||||
{% for field, unlimited, unit in attributes_form.quota_rows %}
|
||||
<div class="col">
|
||||
|
|
@ -122,13 +112,13 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="user-form-section" data-not-superuser{% if superuser == "superuser" %} hidden{% endif %}>
|
||||
<section class="form-section" data-not-superuser{% if superuser == "superuser" %} hidden{% endif %}>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-3">
|
||||
<h2 class="h6 fw-semibold mb-1">{% trans "Groups and permissions" %}</h2>
|
||||
<p class="small text-body-secondary mb-0">{% trans "The permissions of the user's groups apply too." %}</p>
|
||||
</div>
|
||||
<div class="col-lg-9 user-form-fields">
|
||||
<div class="col-lg-9 form-section-fields">
|
||||
<fieldset class="mb-3">
|
||||
<legend class="visually-hidden">{% trans "Groups" %}</legend>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
|
|
@ -165,7 +155,7 @@
|
|||
</section>
|
||||
{% endwith %}
|
||||
|
||||
<div class="user-form-section d-flex justify-content-end gap-2">
|
||||
<div class="form-section d-flex justify-content-end gap-2">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'admin:user_list' %}">{% trans "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-primary">{% if edited_user %}{% trans "Save changes" %}{% else %}{% trans "Create user" %}{% endif %}</button>
|
||||
</div>
|
||||
|
|
|
|||
52
admin/templates/admin/user_password_form.html
Normal file
52
admin/templates/admin/user_password_form.html
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{% extends "base.html" %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Change Password" %}{% endblock %}
|
||||
|
||||
{% block page_heading %}{% trans "Change Password" %}{% endblock page_heading %}
|
||||
|
||||
{% block content %}
|
||||
{# an administrator sets another user's password, or takes it away (sign-in by LDAP, for one) #}
|
||||
<form id="password-change" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form type="non_fields" %}
|
||||
|
||||
<section class="form-section">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-3">
|
||||
<h2 class="h6 fw-semibold mb-1 text-break">{{ edited_user.username }}</h2>
|
||||
<p class="small text-body-secondary mb-0">{% trans "How the user signs in." %}</p>
|
||||
</div>
|
||||
<div class="col-lg-9 form-section-fields">
|
||||
{% trans "Signs in with the password set here." as with_help %}
|
||||
{% trans "Signs in another way, such as LDAP. The current password is removed." as without_help %}
|
||||
<div class="row row-cols-1 row-cols-md-2 g-2 mb-3" role="radiogroup" aria-label="{% trans "How the user signs in." %}">
|
||||
{% for choice in form.usable_password %}
|
||||
<div class="col">
|
||||
<label class="form-choice border rounded p-3 d-flex gap-2 h-100">
|
||||
<input class="form-check-input flex-shrink-0 mt-1" type="radio" name="usable_password" value="{{ choice.data.value }}"{% if choice.data.selected %} checked{% endif %}>
|
||||
<span><span class="d-block fw-semibold">{{ choice.choice_label }}</span>
|
||||
<span class="small text-body-secondary">{% if choice.data.value == "true" %}{{ with_help }}{% else %}{{ without_help }}{% endif %}</span></span>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{# hidden while Without a password is chosen (webvirtcloud.css) #}
|
||||
<div class="with-password">
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">{% bootstrap_field form.password1 %}</div>
|
||||
<div class="col-sm-6">{% bootstrap_field form.password2 show_help=False %}</div>
|
||||
</div>
|
||||
{% bootstrap_field form.must_change_password %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="form-section d-flex justify-content-end gap-2">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'admin:user_update' edited_user.id %}">{% trans "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Change password" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -57,6 +57,17 @@ class UserPagesTests(TestCase):
|
|||
self.assertIn("Manages the panel: every permission, no quota.", page)
|
||||
self.assertNotIn("log into this admin site", page)
|
||||
|
||||
def test_the_password_page_says_how_the_user_signs_in(self):
|
||||
url = reverse("admin:user_update_password", args=[self.alice.pk])
|
||||
page = self.client.get(url).content.decode()
|
||||
self.assertIn("With a password", page)
|
||||
self.assertIn("Signs in another way, such as LDAP. The current password is removed.", page)
|
||||
self.assertIn(f'href="{reverse("admin:user_update", args=[self.alice.pk])}">Cancel', page)
|
||||
# a mismatch is said at its field only
|
||||
response = self.client.post(url, {"usable_password": "true", "password1": "Ab-123456x", "password2": "other"})
|
||||
self.assertContains(response, "The two password fields didn’t match.")
|
||||
self.assertNotIn("Wrong Data Provided", self.messages(response))
|
||||
|
||||
def test_a_refused_choice_is_said_beside_its_field(self):
|
||||
url = reverse("admin:user_update", args=[self.alice.pk])
|
||||
response = self.client.post(url, {"username": "up-alice", "is_active": "on", "groups": "99999", "user_permissions": "99999",
|
||||
|
|
|
|||
|
|
@ -209,19 +209,10 @@ def user_update_password(request, pk):
|
|||
request, _("Password changed for %(user)s") % {"user": user.username}
|
||||
)
|
||||
return redirect("admin:user_list")
|
||||
else:
|
||||
messages.error(request, _("Wrong Data Provided"))
|
||||
else:
|
||||
form = forms.AdminPasswordForm(user)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"accounts/change_password_form.html",
|
||||
{
|
||||
"form": form,
|
||||
"user": user.username,
|
||||
},
|
||||
)
|
||||
return render(request, "admin/user_password_form.html", {"form": form, "edited_user": user})
|
||||
|
||||
|
||||
@superuser_only
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue