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

feat(templates): the shared form and delete pages in a centred column

The pages many views share were a horizontal form whose Cancel went
back in a primary colour beside a green Save, and a delete question in
a yellow alert squeezed into half the width, which wrapped word by word
on a phone.

Both now sit in a centred column with their heading: the form's fields
with their labels above in a card, Cancel outlined and Save the primary
action; the delete page asks "Delete <name>?" in a card with a red
edge, says what deleting does, and offers Cancel and Delete.

The form for a user's rights on a VM names them as the VM's Users tab
does: Change settings, Delete the VM, and Change console settings,
which only counts together with Change settings.
This commit is contained in:
catborise 2026-10-31 14:56:33 +03:00
parent 9790da9931
commit 488a77b3c7
5 changed files with 49 additions and 33 deletions

View file

@ -32,6 +32,10 @@ class UserInstanceForm(ModelForm):
class Meta:
model = UserInstance
fields = "__all__"
# the rights beyond power and console, as the VM's Users tab names them
labels = {"is_change": _("Change settings"), "is_delete": _("Delete the VM"),
"is_vnc": _("Change console settings")}
help_texts = {"is_vnc": _("Only together with Change settings")}
class UniqueEmailMixin:

View file

@ -71,3 +71,12 @@ class AccountPageTests(TestCase):
url = reverse("accounts:account_ssh_key_delete", args=[self.alice.pk, self.key.pk])
self.assertEqual(self.client.post(url).status_code, 403)
self.assertTrue(UserSSHKey.objects.exists())
def test_the_owner_form_names_the_rights_as_the_vm_page_does(self):
compute = Compute.objects.create(name="ap-host2", hostname="localhost", login="", password="", details="", type=4)
vm = Instance.objects.create(compute=compute, name="ap-vm2", uuid="00000000-0000-0000-0000-000000000002")
owner = UserInstance.objects.create(user=self.alice, instance=vm)
response = self.client.get(reverse("accounts:user_instance_update", args=[owner.pk]))
for label in ("Change settings", "Delete the VM", "Change console settings"):
self.assertContains(response, label)
self.assertNotContains(response, "Is vnc")

View file

@ -1,29 +1,31 @@
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% load bootstrap_icons %}
{% load i18n %}
{% block title %}{% trans "Delete" %} {{ object }}{% endblock %}
{% block page_heading %}{% trans "Delete" %} {{ object }}{% endblock page_heading %}
{% block page_header %}
<div class="row justify-content-center">
<div class="col-lg-8"><h3 class="mb-3 text-break">{% trans "Delete" %} {{ object }}</h3></div>
</div>
{% endblock page_header %}
{% block content %}
<div class="row">
<div class="offset-3 col-6">
<div class="row justify-content-center">
<div class="col-lg-8">
<form method="post">
{% csrf_token %}
<div class="alert alert-warning">
{% blocktrans %}Delete "{{ object }}"?{% endblocktrans %} {{ consequence }}
<div class="card mb-3">
<div class="card-body border-start border-danger border-4 rounded-start">
<h4 class="h5 text-break">{% bs_icon 'trash' %} {% blocktrans %}Delete <strong>{{ object }}</strong>?{% endblocktrans %}</h4>
{% if consequence %}<p class="mb-0">{{ consequence }}</p>{% endif %}
</div>
</div>
<div class="mb-0 float-end">
<a class="btn btn-primary" href="javascript:history.back()">
{% bs_icon 'x-circle-fill' %} {% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger">
{% bs_icon 'trash-fill' %} {% trans "Delete" %}
</button>
<div class="d-flex flex-wrap justify-content-end gap-2">
<a class="btn btn-outline-secondary" href="javascript:history.back()">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% endblock %}

View file

@ -1,27 +1,28 @@
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% load bootstrap_icons %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block page_heading %}{{ title }}{% endblock page_heading %}
{% block page_header %}
<div class="row justify-content-center">
<div class="col-lg-8"><h3 class="mb-3">{{ title }}</h3></div>
</div>
{% endblock page_header %}
{% block content %}
<div class="card">
<div class="card-body">
<form id="create-update" action="" method="post">
<div class="row justify-content-center">
<div class="col-lg-8">
<form method="post">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
<div class="card mb-3">
<div class="card-body">{% bootstrap_form form %}</div>
</div>
<div class="d-flex flex-wrap justify-content-end gap-2">
<a class="btn btn-outline-secondary" href="javascript:history.back()">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
</div>
</form>
</div>
<div class="card-footer">
<div class="mb-0 float-end">
<a class="btn btn-primary" href="javascript:history.back()">{% bs_icon 'x' %} {% trans "Cancel" %}</a>
<button type="submit" form="create-update" class="btn btn-success">
{% bs_icon 'check2' %} {% trans "Save" %}
</button>
</div>
</div>
</div>
{% endblock content %}
{% endblock content %}

View file

@ -34,7 +34,7 @@ class DeletePageTests(TestCase):
self.client.force_login(admin)
for url, consequence in pages.items():
with self.subTest(url=url):
alert = html.fromstring(self.client.get(url).content).xpath("//div[contains(@class, 'alert-warning')]")
self.assertEqual(len(alert), 1)
self.assertIn(consequence, alert[0].text_content())
self.assertNotIn("Are you sure", alert[0].text_content())
card = html.fromstring(self.client.get(url).content).xpath("//div[contains(@class, 'border-danger')]")
self.assertEqual(len(card), 1)
self.assertIn(consequence, card[0].text_content())
self.assertNotIn("Are you sure", card[0].text_content())