mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
feat: error pages that say what happened and what to do
The 403, 404 and 500 pages said "Oops!" and one generic sentence; a bad request and a failed CSRF check fell back to Django's plain pages. Every error page now shares templates/error.html: a card with the status in a coloured badge with an icon, a heading in plain words, what happened and what to do, and Back and Home buttons. It uses only theme-defined Bootstrap classes, so it reads in the light and dark themes alike. - 400: the request was not accepted; a new address belongs in ALLOWED_HOSTS. - 403: the account lacks the permission; ask an administrator. - 403 CSRF: the form's security check failed; reload and submit again, and where to look when it happens on every form. - 404: the path, and that the object may be gone or hidden from the account. - 500: a libvirt failure on a page says the host's data could not be loaded, under libvirt's message; any other failure says it was logged and shows the time to give an administrator.
This commit is contained in:
parent
c3c84d780c
commit
10eb0d1c2e
8 changed files with 140 additions and 33 deletions
13
templates/400.html
Normal file
13
templates/400.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Bad request" %}{% endblock %}
|
||||
{% block code %}400{% endblock %}
|
||||
{% block status_classes %}text-bg-warning{% endblock %}
|
||||
{% block icon %}bi-exclamation-triangle{% endblock %}
|
||||
{% block heading %}{% trans "Bad request" %}{% endblock %}
|
||||
|
||||
{% block text %}
|
||||
<p class="mb-3">{% trans "The panel could not accept this request." %}</p>
|
||||
<p class="text-body-secondary mb-0">{% trans "If the panel was opened at a new address, an administrator must add that address to ALLOWED_HOSTS." %}</p>
|
||||
{% endblock %}
|
||||
|
|
@ -1,15 +1,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "403" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<h1>{% trans 'Oops!'%}</h1>
|
||||
|
||||
<p class="lead">{% trans "403 Forbidden" %}</p>
|
||||
{% block title %}{% trans "Access denied" %}{% endblock %}
|
||||
{% block code %}403{% endblock %}
|
||||
{% block status_classes %}text-bg-warning{% endblock %}
|
||||
{% block icon %}bi-shield-lock{% endblock %}
|
||||
{% block heading %}{% trans "Access denied" %}{% endblock %}
|
||||
|
||||
<p>{% trans "You do not have permission to access this page." %}</p>
|
||||
<a class="btn btn-success" href="javascript:history.back()">← {% trans 'Back'%}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% block text %}
|
||||
<p class="mb-0">{% trans "Your account does not have the permission for this page or action. Ask an administrator for it." %}</p>
|
||||
{% endblock %}
|
||||
|
|
|
|||
14
templates/403_csrf.html
Normal file
14
templates/403_csrf.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Form could not be submitted" %}{% endblock %}
|
||||
{% block code %}403{% endblock %}
|
||||
{% block status_classes %}text-bg-warning{% endblock %}
|
||||
{% block icon %}bi-shield-exclamation{% endblock %}
|
||||
{% block heading %}{% trans "Form could not be submitted" %}{% endblock %}
|
||||
|
||||
{% block text %}
|
||||
<p class="mb-3">{% trans "This submission was rejected because its security check failed. It usually happens when the page was open for a long time, or after signing in or out in another tab." %}</p>
|
||||
<p class="fw-semibold mb-3">{% trans "Go back, reload the page and submit the form again." %}</p>
|
||||
<p class="small text-body-secondary border-top pt-3 mb-0">{% trans "If it happens on every form, the browser may block cookies, or the panel's proxy or address settings may be wrong. The reason is written to the panel's log." %}</p>
|
||||
{% endblock %}
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "404" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<h1>{% trans 'Oops!'%}</h1>
|
||||
|
||||
<p class="lead">{% trans "404 Not Found" %}</p>
|
||||
{% block title %}{% trans "Page not found" %}{% endblock %}
|
||||
{% block code %}404{% endblock %}
|
||||
{% block status_classes %}text-bg-secondary{% endblock %}
|
||||
{% block icon %}bi-file-earmark-x{% endblock %}
|
||||
{% block heading %}{% trans "Page not found" %}{% endblock %}
|
||||
|
||||
<p>{% trans "The requested page was not found on this server." %}</p>
|
||||
<a class="btn btn-success" href="javascript:history.back()">← {% trans 'Back'%}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% block text %}
|
||||
<p class="mb-3">{% blocktrans with path=request_path %}Nothing was found at <code>{{ path }}</code>, or your account cannot see it.{% endblocktrans %}</p>
|
||||
<p class="text-body-secondary mb-0">{% trans "The virtual machine or host may have been deleted, or the link is wrong." %}</p>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "500" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<h1>{% trans 'Oops!' %}</h1>
|
||||
|
||||
<p class="lead">{% trans "500 Internal Server Error" %}</p>
|
||||
{% block title %}{% if libvirt_error %}{% trans "Could not load data from the host" %}{% else %}{% trans "Server error" %}{% endif %}{% endblock %}
|
||||
{% block code %}500{% endblock %}
|
||||
{% block status_classes %}text-bg-danger{% endblock %}
|
||||
{% block icon %}{% if libvirt_error %}bi-hdd-network{% else %}bi-exclamation-octagon{% endif %}{% endblock %}
|
||||
{% block heading %}{% if libvirt_error %}{% trans "Could not load data from the host" %}{% else %}{% trans "Server error" %}{% endif %}{% endblock %}
|
||||
|
||||
<p>{% trans "The server encountered an internal error or misconfiguration and was unable to complete you request." %}</p>
|
||||
<a class="btn btn-success" href="javascript:history.back()">← {% trans 'Back'%}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% block text %}
|
||||
{% if libvirt_error %}
|
||||
<p class="mb-3">{% trans "This page needs data from the host, and libvirt returned the error shown above." %}</p>
|
||||
<p class="text-body-secondary mb-0">{% trans "Check that the host is running and reachable, then reload the page." %}</p>
|
||||
{% else %}
|
||||
<p class="mb-3">{% trans "The panel failed while handling this request. This is a fault in the panel, not something you did, and it was written to the panel's log." %}</p>
|
||||
<p class="text-body-secondary mb-0">{% now "Y-m-d H:i:s" as now %}{% blocktrans %}Reload the page to try again. If it keeps happening, tell an administrator the time: {{ now }}.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
39
templates/error.html
Normal file
39
templates/error.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center py-3 py-sm-4">
|
||||
<div class="col-12 col-lg-10">
|
||||
<section class="card shadow-sm" aria-labelledby="error-heading">
|
||||
<div class="card-body p-4 p-sm-5">
|
||||
<div class="mb-3">
|
||||
<span class="badge d-inline-flex align-items-center gap-2 rounded-pill fs-6 px-3 py-2 {% block status_classes %}text-bg-secondary{% endblock %}">
|
||||
<i class="bi {% block icon %}bi-exclamation-circle{% endblock %}" aria-hidden="true"></i>
|
||||
<span class="font-monospace">
|
||||
<span class="visually-hidden">{% trans "HTTP status" %} </span>
|
||||
{% block code %}{% endblock %}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 id="error-heading" class="h3 mb-3">{% block heading %}{% endblock %}</h1>
|
||||
|
||||
<div class="text-break">
|
||||
{% block text %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="d-grid d-sm-flex gap-2 mt-4">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">
|
||||
<i class="bi bi-arrow-left me-1" aria-hidden="true"></i>
|
||||
{% trans "Back" %}
|
||||
</a>
|
||||
<a class="btn btn-secondary" href="{% url 'index' %}">
|
||||
<i class="bi bi-house me-1" aria-hidden="true"></i>
|
||||
{% trans "Home" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -66,6 +66,8 @@ class ConnectionErrorPageTests(TestCase):
|
|||
response, messages = self.handle(self.admin, libvirt_error(libvirt.VIR_FROM_RPC, HOST_DETAIL),
|
||||
path="/instances/1/", method="get")
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertIn(b"Could not load data from the host", response.content)
|
||||
self.assertIn(HOST_DETAIL.encode(), response.content)
|
||||
self.assertTrue(messages)
|
||||
|
||||
def test_administrators_see_connection_details(self):
|
||||
|
|
|
|||
41
webvirtcloud/tests/test_error_pages.py
Normal file
41
webvirtcloud/tests/test_error_pages.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
"""The error pages say what happened and what to do, in the panel's layout."""
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.http import Http404
|
||||
from django.test import Client, RequestFactory, TestCase
|
||||
from django.views.defaults import page_not_found, server_error
|
||||
|
||||
|
||||
class ErrorPageTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create_user("ep-user", password="pw")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_a_bad_request(self):
|
||||
response = self.client.get("/", HTTP_HOST="unknown.example")
|
||||
self.assertContains(response, "Bad request", status_code=400)
|
||||
self.assertContains(response, "ALLOWED_HOSTS", status_code=400)
|
||||
|
||||
def test_a_page_the_user_may_not_open(self):
|
||||
response = self.client.get("/computes/")
|
||||
self.assertContains(response, "Access denied", status_code=403)
|
||||
self.assertContains(response, "Ask an administrator", status_code=403)
|
||||
|
||||
def test_a_form_that_fails_its_security_check(self):
|
||||
response = Client(enforce_csrf_checks=True).post("/accounts/login/", {"username": "ep-user", "password": "pw"})
|
||||
self.assertContains(response, "Form could not be submitted", status_code=403)
|
||||
self.assertContains(response, "reload the page and submit the form again", status_code=403)
|
||||
|
||||
def test_a_page_that_is_not_there_names_its_path(self):
|
||||
response = self.client.get("/no/such/page/")
|
||||
self.assertContains(response, "Page not found", status_code=404)
|
||||
self.assertContains(response, "<code>/no/such/page/</code>", status_code=404)
|
||||
|
||||
def test_the_reason_of_a_not_found_is_not_shown(self):
|
||||
response = page_not_found(RequestFactory().get("/datasource/"), Http404("libvirt detail of the host"))
|
||||
self.assertNotIn(b"libvirt detail", response.content)
|
||||
|
||||
def test_a_server_error_renders_without_a_request_context(self):
|
||||
response = server_error(RequestFactory().get("/"))
|
||||
self.assertContains(response, "Server error", status_code=500)
|
||||
self.assertContains(response, "tell an administrator the time", status_code=500)
|
||||
Loading…
Add table
Add a link
Reference in a new issue