mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-08-26 12:46:08 +00:00
ui: Handle and show most page load errors as alerts
- In addition to the OperationalError, also handle all generic exceptions during page submit and page load. Redirect to the same page or parent using breadcrumbs. - Log exceptions handled by common error middleware so that they are also part of the system logs. - Update kiwix test as needed. - Refactor some test code that is setting up the menu items. Tests: - When an error occurs during form POST, the same page is show but with an error message. - When an error occurs in an app page during GET, the browser is redirected to the parent section. - When an error occurs in apps page during GET, the browser is redirected to the home page. - When an error occurs in home page during GET, the error is not handled and default 500 handle is triggered. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Veiko Aasa <veiko17@disroot.org>
This commit is contained in:
parent
59a0a3b25f
commit
407fccba2f
@ -266,3 +266,17 @@ def fixture_host_sudo(host):
|
|||||||
"""Pytest fixture to run commands with sudo."""
|
"""Pytest fixture to run commands with sudo."""
|
||||||
with host.sudo():
|
with host.sudo():
|
||||||
yield host
|
yield host
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name='test_menu')
|
||||||
|
def fixture_test_menu():
|
||||||
|
"""Initialized menu module."""
|
||||||
|
from plinth import menu as menu_module
|
||||||
|
|
||||||
|
menu_module.Menu._all_menus = set()
|
||||||
|
menu_module.init()
|
||||||
|
menu_module.Menu('home-id', name='Home', url_name='index')
|
||||||
|
menu_module.Menu('apps-id', name='Apps', url_name='apps',
|
||||||
|
parent_url_name='index')
|
||||||
|
menu_module.Menu('testapp-id', name='Test App', url_name='testapp:index',
|
||||||
|
parent_url_name='apps')
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db.utils import OperationalError
|
from django.db.utils import OperationalError
|
||||||
from django.shortcuts import render
|
from django.shortcuts import redirect, render
|
||||||
from django.template.response import SimpleTemplateResponse
|
from django.template.response import SimpleTemplateResponse
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -126,6 +126,8 @@ class CommonErrorMiddleware(MiddlewareMixin):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def process_exception(request, exception):
|
def process_exception(request, exception):
|
||||||
"""Show a custom error page when OperationalError is raised."""
|
"""Show a custom error page when OperationalError is raised."""
|
||||||
|
logger.exception('Error processing page. %s %s, exception: %s',
|
||||||
|
request.method, request.path, exception)
|
||||||
if isinstance(exception, OperationalError):
|
if isinstance(exception, OperationalError):
|
||||||
message = _(
|
message = _(
|
||||||
'System is possibly under heavy load. Please retry later.')
|
'System is possibly under heavy load. Please retry later.')
|
||||||
@ -133,4 +135,34 @@ class CommonErrorMiddleware(MiddlewareMixin):
|
|||||||
context={'message': message},
|
context={'message': message},
|
||||||
status=503)
|
status=503)
|
||||||
|
|
||||||
|
if isinstance(exception, Exception):
|
||||||
|
match = request.resolver_match
|
||||||
|
if not match.app_name and match.url_name == 'index':
|
||||||
|
# Don't try to handle errors on the home page as it will lead
|
||||||
|
# to infinite redirects.
|
||||||
|
return None
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
message = _('Error running operation.')
|
||||||
|
else:
|
||||||
|
message = _('Error loading page.')
|
||||||
|
|
||||||
|
views.messages_error(request, message, exception)
|
||||||
|
redirect_url = CommonErrorMiddleware._get_redirect_url_on_error(
|
||||||
|
request)
|
||||||
|
return redirect(redirect_url)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_redirect_url_on_error(request):
|
||||||
|
"""Return the URL to redirect to after an error."""
|
||||||
|
if request.method != 'GET':
|
||||||
|
return request.path
|
||||||
|
|
||||||
|
# If the original request was a GET, trying to redirect to same URL
|
||||||
|
# with same request method might result in an recursive loop. Instead
|
||||||
|
# redirect to a parent URL.
|
||||||
|
breadcrumbs = views.get_breadcrumbs(request)
|
||||||
|
parent_index = 1 if len(breadcrumbs) > 1 else 0
|
||||||
|
return list(breadcrumbs.keys())[parent_index]
|
||||||
|
|||||||
@ -20,9 +20,7 @@ from django.conf import settings
|
|||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
|
|
||||||
from plinth import cfg
|
from plinth import cfg, module_loader
|
||||||
from plinth import menu as menu_module
|
|
||||||
from plinth import module_loader
|
|
||||||
from plinth.modules import help as help_module
|
from plinth.modules import help as help_module
|
||||||
from plinth.modules.help import views
|
from plinth.modules.help import views
|
||||||
|
|
||||||
@ -57,9 +55,8 @@ def fixture_developer_configuration():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name='menu', autouse=True)
|
@pytest.fixture(name='menu', autouse=True)
|
||||||
def fixture_menu():
|
def fixture_menu(test_menu):
|
||||||
"""Initialized menu module."""
|
"""Initialized menu module."""
|
||||||
menu_module.init()
|
|
||||||
help_module.HelpApp() # Create all menu components
|
help_module.HelpApp() # Create all menu components
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,12 +3,12 @@
|
|||||||
Test module for Kiwix views.
|
Test module for Kiwix views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest.mock import call, patch, MagicMock
|
from unittest.mock import MagicMock, call, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django import urls
|
from django import urls
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
||||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
|
|
||||||
from plinth import module_loader
|
from plinth import module_loader
|
||||||
@ -94,7 +94,8 @@ def test_add_package_failed(add_package, add_package_request):
|
|||||||
views.AddPackageView.as_view())
|
views.AddPackageView.as_view())
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
assert response.url == urls.reverse('kiwix:index')
|
assert response.url == urls.reverse('kiwix:index')
|
||||||
assert list(messages)[0].message == 'Failed to add content package.'
|
assert list(messages)[0].message.startswith(
|
||||||
|
'Failed to add content package.')
|
||||||
|
|
||||||
|
|
||||||
@patch('plinth.app.App.get')
|
@patch('plinth.app.App.get')
|
||||||
|
|||||||
@ -5,27 +5,13 @@ Test module for custom context processors.
|
|||||||
|
|
||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
from django.urls import resolve
|
from django.urls import resolve
|
||||||
|
|
||||||
from plinth import context_processors as cp
|
from plinth import context_processors as cp
|
||||||
from plinth import menu as menu_module
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name='menu', autouse=True)
|
|
||||||
def fixture_menu():
|
|
||||||
"""Initialized menu module."""
|
|
||||||
menu_module.Menu._all_menus = set()
|
|
||||||
menu_module.init()
|
|
||||||
menu_module.Menu('home-id', name='Home', url_name='index')
|
|
||||||
menu_module.Menu('apps-id', name='Apps', url_name='apps',
|
|
||||||
parent_url_name='index')
|
|
||||||
menu_module.Menu('testapp-id', name='Test App', url_name='testapp:index',
|
|
||||||
parent_url_name='apps')
|
|
||||||
|
|
||||||
|
|
||||||
@patch('plinth.notification.Notification')
|
@patch('plinth.notification.Notification')
|
||||||
def test_common(Notification, load_cfg, rf):
|
def test_common(Notification, load_cfg, rf, test_menu):
|
||||||
"""Verify that the common() function returns the correct values."""
|
"""Verify that the common() function returns the correct values."""
|
||||||
url = '/apps/testapp/create/'
|
url = '/apps/testapp/create/'
|
||||||
request = rf.get(url)
|
request = rf.get(url)
|
||||||
|
|||||||
@ -9,8 +9,9 @@ import pytest
|
|||||||
from django.contrib.auth.models import AnonymousUser, Group, User
|
from django.contrib.auth.models import AnonymousUser, Group, User
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db.utils import OperationalError
|
from django.db.utils import OperationalError
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
from django.urls import resolve
|
||||||
from stronghold.decorators import public
|
from stronghold.decorators import public
|
||||||
|
|
||||||
from plinth import app as app_module
|
from plinth import app as app_module
|
||||||
@ -144,12 +145,14 @@ class TestSetupMiddleware:
|
|||||||
resolve.return_value.namespaces = ['mockapp']
|
resolve.return_value.namespaces = ['mockapp']
|
||||||
operation_manager.collect_results.return_value = [
|
operation_manager.collect_results.return_value = [
|
||||||
Mock(translated_message='message1', exception=None),
|
Mock(translated_message='message1', exception=None),
|
||||||
Mock(translated_message='message2', exception='x-exception')
|
Mock(translated_message='message2',
|
||||||
|
exception=RuntimeError('x-exception'))
|
||||||
]
|
]
|
||||||
app.get_setup_state = lambda: app_module.App.SetupState.UP_TO_DATE
|
app.get_setup_state = lambda: app_module.App.SetupState.UP_TO_DATE
|
||||||
|
|
||||||
# Admin user can collect result
|
# Admin user can collect result
|
||||||
request = RequestFactory().get('/plinth/mockapp')
|
request = RequestFactory().get('/plinth/mockapp')
|
||||||
|
request.resolver_match = Mock()
|
||||||
user = User(username='adminuser')
|
user = User(username='adminuser')
|
||||||
user.save()
|
user.save()
|
||||||
group = Group(name='admin')
|
group = Group(name='admin')
|
||||||
@ -162,7 +165,9 @@ class TestSetupMiddleware:
|
|||||||
|
|
||||||
assert response is None
|
assert response is None
|
||||||
messages_success.assert_has_calls([call(request, 'message1')])
|
messages_success.assert_has_calls([call(request, 'message1')])
|
||||||
messages_error.assert_has_calls([call(request, 'message2')])
|
messages_error.assert_called_once()
|
||||||
|
assert messages_error.call_args.args[0] == request
|
||||||
|
assert messages_error.call_args.args[1].startswith('message2')
|
||||||
operation_manager.collect_results.assert_has_calls([call('mockapp')])
|
operation_manager.collect_results.assert_has_calls([call('mockapp')])
|
||||||
|
|
||||||
# Non-admin user can't collect result
|
# Non-admin user can't collect result
|
||||||
@ -265,7 +270,8 @@ class TestCommonErrorMiddleware:
|
|||||||
@pytest.fixture(name='web_request')
|
@pytest.fixture(name='web_request')
|
||||||
def fixture_web_request():
|
def fixture_web_request():
|
||||||
"""Fixture for returning web request."""
|
"""Fixture for returning web request."""
|
||||||
web_request = RequestFactory().get('/plinth/mockapp')
|
web_request = RequestFactory().get('/apps/testapp/')
|
||||||
|
web_request.resolver_match = resolve('/apps/testapp/')
|
||||||
web_request.user = Mock()
|
web_request.user = Mock()
|
||||||
return web_request
|
return web_request
|
||||||
|
|
||||||
@ -288,6 +294,36 @@ class TestCommonErrorMiddleware:
|
|||||||
assert 'message' in response.context_data
|
assert 'message' in response.context_data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def test_other_error(middleware, web_request, other_error):
|
@patch('django.contrib.messages.error')
|
||||||
|
def test_other_error_get(messages_error, middleware, web_request,
|
||||||
|
other_error, test_menu):
|
||||||
response = middleware.process_exception(web_request, other_error)
|
response = middleware.process_exception(web_request, other_error)
|
||||||
assert response is None
|
assert isinstance(response, HttpResponseRedirect)
|
||||||
|
assert response.url == '/apps/'
|
||||||
|
messages_error.assert_called_once()
|
||||||
|
assert messages_error.call_args.args[0] == web_request
|
||||||
|
assert messages_error.call_args.args[1].startswith(
|
||||||
|
'Error loading page.')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@patch('django.contrib.messages.error')
|
||||||
|
def test_other_error_post(messages_error, middleware, web_request,
|
||||||
|
other_error, test_menu):
|
||||||
|
web_request.method = 'POST'
|
||||||
|
response = middleware.process_exception(web_request, other_error)
|
||||||
|
assert isinstance(response, HttpResponseRedirect)
|
||||||
|
assert response.url == '/apps/testapp/'
|
||||||
|
messages_error.assert_called_once()
|
||||||
|
assert messages_error.call_args.args[0] == web_request
|
||||||
|
assert messages_error.call_args.args[1].startswith(
|
||||||
|
'Error running operation.')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@patch('django.contrib.messages.error')
|
||||||
|
def test_other_error_index(messages_error, middleware, web_request,
|
||||||
|
other_error, test_menu):
|
||||||
|
web_request.path = '/'
|
||||||
|
web_request.resolver_match = resolve('/')
|
||||||
|
response = middleware.process_exception(web_request, other_error)
|
||||||
|
assert not response
|
||||||
|
messages_error.assert_not_called()
|
||||||
|
|||||||
@ -6,23 +6,10 @@ Tests for common FreedomBox views.
|
|||||||
import pytest
|
import pytest
|
||||||
from django.urls import resolve
|
from django.urls import resolve
|
||||||
|
|
||||||
from plinth import menu as menu_module
|
|
||||||
from plinth.views import get_breadcrumbs, is_safe_url
|
from plinth.views import get_breadcrumbs, is_safe_url
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name='menu')
|
def test_get_breadcrumbs(rf, test_menu):
|
||||||
def fixture_menu():
|
|
||||||
"""Initialized menu module."""
|
|
||||||
menu_module.Menu._all_menus = set()
|
|
||||||
menu_module.init()
|
|
||||||
menu_module.Menu('home-id', name='Home', url_name='index')
|
|
||||||
menu_module.Menu('apps-id', name='Apps', url_name='apps',
|
|
||||||
parent_url_name='index')
|
|
||||||
menu_module.Menu('testapp-id', name='Test App', url_name='testapp:index',
|
|
||||||
parent_url_name='apps')
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_breadcrumbs(rf, menu):
|
|
||||||
"""Test that computing breadcrumbs works."""
|
"""Test that computing breadcrumbs works."""
|
||||||
|
|
||||||
def _crumb(name: str, is_active: bool = False, url_name: str | None = None,
|
def _crumb(name: str, is_active: bool = False, url_name: str | None = None,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user