diff --git a/plinth/tests/data/urls.py b/plinth/tests/data/urls.py index cfa25921c..8482c655f 100644 --- a/plinth/tests/data/urls.py +++ b/plinth/tests/data/urls.py @@ -3,13 +3,20 @@ Django URL patterns for running tests. """ -from django.urls import re_path +from django.urls import include, re_path from django.views.generic import TemplateView _test_view = TemplateView.as_view(template_name='index.html') + +app_urls = [ + re_path(r'^apps/testapp/$', _test_view, name='index'), + re_path(r'^apps/testapp/create/$', _test_view, name='create'), +] + urlpatterns = [ re_path(r'^$', _test_view, name='index'), re_path(r'^apps/$', _test_view, name='apps'), + re_path(r'', include((app_urls, 'testapp'))), re_path(r'^sys/$', _test_view, name='system'), re_path(r'^test/(?P\d+)/(?P\d+)/(?P\d+)/$', _test_view, name='test'), diff --git a/plinth/tests/test_views.py b/plinth/tests/test_views.py index 08668aa13..e6d7e4bea 100644 --- a/plinth/tests/test_views.py +++ b/plinth/tests/test_views.py @@ -4,8 +4,69 @@ Tests for common FreedomBox views. """ import pytest +from django.urls import resolve -from plinth.views import is_safe_url +from plinth import menu as menu_module +from plinth.views import get_breadcrumbs, is_safe_url + + +@pytest.fixture(name='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.""" + + def _crumb(name: str, is_active: bool = False, url_name: str | None = None, + is_active_section: bool = False): + crumb = {'name': name, 'is_active': is_active, 'url_name': url_name} + if is_active_section: + crumb['is_active_section'] = True + + return crumb + + def _get(path: str): + request = rf.get(path) + request.resolver_match = resolve(path) + return get_breadcrumbs(request) + + def _compare(dict1: dict[str, dict[str, str | bool]], + dict2: dict[str, dict[str, str | bool]]): + """Compare dictionaries with order.""" + assert list(dict1.items()) == list(dict2.items()) + + _compare(_get('/'), {'/': _crumb('Home', True, 'index', True)}) + _compare( + _get('/apps/'), { + '/apps/': _crumb('Apps', True, 'apps', True), + '/': _crumb('Home', False, 'index'), + }) + _compare( + _get('/apps/testapp/'), { + '/apps/testapp/': _crumb('Test App', True, 'testapp:index'), + '/apps/': _crumb('Apps', False, 'apps', True), + '/': _crumb('Home', False, 'index'), + }) + _compare( + _get('/apps/testapp/create/'), { + '/apps/testapp/create/': _crumb('Here', True, 'testapp:create'), + '/apps/testapp/': _crumb('Test App', False, 'testapp:index'), + '/apps/': _crumb('Apps', False, 'apps', True), + '/': _crumb('Home', False, 'index'), + }) + _compare( + _get('/test/1/2/3/'), { + '/test/1/2/3/': _crumb('Here', True, 'test', True), + '/': _crumb('Home', False, 'index'), + }) @pytest.mark.parametrize('url', [ diff --git a/plinth/views.py b/plinth/views.py index 1b4337f93..856a8485e 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -11,7 +11,8 @@ import urllib.parse from django.contrib import messages from django.core.exceptions import ImproperlyConfigured from django.forms import Form -from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect +from django.http import (Http404, HttpRequest, HttpResponseBadRequest, + HttpResponseRedirect) from django.shortcuts import redirect from django.template.response import TemplateResponse from django.urls import reverse @@ -62,6 +63,53 @@ def is_safe_url(url): return True +def get_breadcrumbs(request: HttpRequest) -> dict[str, dict[str, str | bool]]: + """Return all the URL ancestors that can be show as breadcrumbs.""" + breadcrumbs = {} + + def _add(url: str, name: str, url_name: str | None = None): + """Add item into the breadcrumb dictionary.""" + breadcrumbs[url] = { + 'name': name, + 'is_active': request.path == url, + 'url_name': url_name + } + + url_name = request.resolver_match.url_name + full_url_name = ':'.join(request.resolver_match.app_names + [url_name]) + try: + menu_item = menu.Menu.get_with_url_name(full_url_name) + except LookupError: + # There is no menu entry for this page, find it's app. + _add(request.path, _('Here'), full_url_name) + app_url_name = ':'.join(request.resolver_match.app_names + ['index']) + try: + menu_item = menu.Menu.get_with_url_name(app_url_name) + except LookupError: + # Don't know which app this page belongs to, assume parent is Home. + menu_item = menu.Menu.get_with_url_name('index') + + for _number in range(10): + _add(menu_item.url, menu_item.name, menu_item.url_name) + if not menu_item.parent_url_name: + # We have reached the top + break + + menu_item = menu.Menu.get_with_url_name(menu_item.parent_url_name) + else: + # Too much hierarchy, we must be in a recursive loop. + breadcrumbs = {} + menu_item = menu.Menu.get_with_url_name('index') + _add(menu_item.url, menu_item.name, menu_item.url_name) + + # Find the active section: 'index', 'apps', 'system' or 'help'. + active_section_index = -2 if len(breadcrumbs) >= 2 else -1 + active_section_key = list(breadcrumbs.keys())[active_section_index] + breadcrumbs[active_section_key]['is_active_section'] = True + + return breadcrumbs + + def messages_error(request, message, exception): """Show an error message using Django messages framework.