diff --git a/bugsink/app_settings.py b/bugsink/app_settings.py index 5bd1ce4..c855b17 100644 --- a/bugsink/app_settings.py +++ b/bugsink/app_settings.py @@ -2,6 +2,7 @@ # alternative would be: just put "everything" in the big settings.py (or some mix-using-imports version of that). # but for now I like the idea of keeping the bugsink-as-an-app stuff separate from the regular Django/db/global stuff. import os +import urllib.parse from contextlib import contextmanager from django.conf import settings @@ -110,6 +111,22 @@ def _sanitize(settings): settings["TEAM_CREATION"] = CB_NOBODY +def get_path_prefix(): + """Extract the path prefix from BASE_URL for subpath hosting support.""" + base_url = get_settings().BASE_URL + parsed_url = urllib.parse.urlparse(base_url) + path = parsed_url.path + + # Ensure path starts with / and doesn't end with / (unless it's just "/") + if path and path != "/": + if not path.startswith("/"): + path = "/" + path + if path.endswith("/"): + path = path[:-1] + return path + return "" + + def get_settings(): global _settings if _settings is None: diff --git a/bugsink/context_processors.py b/bugsink/context_processors.py index 172560e..e1ea690 100644 --- a/bugsink/context_processors.py +++ b/bugsink/context_processors.py @@ -10,7 +10,7 @@ from django.contrib.auth.models import AnonymousUser from django.db.utils import OperationalError from django.db.models import Sum -from bugsink.app_settings import get_settings, CB_ANYBODY +from bugsink.app_settings import get_settings, CB_ANYBODY, get_path_prefix from bugsink.transaction import durable_atomic from bugsink.timed_sqlite_backend.base import different_runtime_limit @@ -135,6 +135,7 @@ def useful_settings_processor(request): 'site_title': get_settings().SITE_TITLE, 'registration_enabled': get_settings().USER_REGISTRATION == CB_ANYBODY, 'app_settings': get_settings(), + 'path_prefix': get_path_prefix(), 'system_warnings': get_system_warnings, } diff --git a/bugsink/settings/default.py b/bugsink/settings/default.py index f4881fe..6356a56 100644 --- a/bugsink/settings/default.py +++ b/bugsink/settings/default.py @@ -343,3 +343,10 @@ if I_AM_RUNNING == "SNAPPEA": for logger in LOGGING['loggers'].values(): if "handlers" in logger and "console" in logger["handlers"]: logger["handlers"] = ["snappea"] + +# Set FORCE_SCRIPT_NAME for subpath hosting support +# Import here to avoid circular dependency +from bugsink.app_settings import get_path_prefix +_path_prefix = get_path_prefix() +if _path_prefix: + FORCE_SCRIPT_NAME = _path_prefix diff --git a/bugsink/test_subpath.py b/bugsink/test_subpath.py new file mode 100644 index 0000000..2936c72 --- /dev/null +++ b/bugsink/test_subpath.py @@ -0,0 +1,67 @@ +from django.test import TestCase, override_settings +from django.template import Template, Context + +from bugsink.app_settings import get_path_prefix, override_settings as override_bugsink_settings +from bugsink.context_processors import useful_settings_processor + + +class SubpathHostingTestCase(TestCase): + """Test subpath hosting functionality.""" + + def test_get_path_prefix_default(self): + """Test that get_path_prefix returns empty string for default BASE_URL.""" + with override_bugsink_settings(BASE_URL="http://localhost:8000"): + self.assertEqual(get_path_prefix(), "") + + def test_get_path_prefix_subpath(self): + """Test that get_path_prefix extracts path from BASE_URL.""" + with override_bugsink_settings(BASE_URL="https://example.com/bugsink"): + self.assertEqual(get_path_prefix(), "/bugsink") + + def test_get_path_prefix_subpath_with_trailing_slash(self): + """Test that get_path_prefix handles trailing slash correctly.""" + with override_bugsink_settings(BASE_URL="https://example.com/bugsink/"): + self.assertEqual(get_path_prefix(), "/bugsink") + + def test_get_path_prefix_deeper_path(self): + """Test that get_path_prefix handles deeper paths.""" + with override_bugsink_settings(BASE_URL="https://example.com/tools/bugsink"): + self.assertEqual(get_path_prefix(), "/tools/bugsink") + + def test_context_processor_includes_path_prefix(self): + """Test that context processor includes path prefix.""" + from django.http import HttpRequest + + request = HttpRequest() + request.user = None # Mock an anonymous user + + with override_bugsink_settings(BASE_URL="https://example.com/bugsink"): + context = useful_settings_processor(request) + self.assertEqual(context['path_prefix'], "/bugsink") + + def test_template_filter_with_prefix(self): + """Test the with_prefix template filter.""" + template = Template('{% load urls %}{{ url | with_prefix }}') + + with override_bugsink_settings(BASE_URL="https://example.com/bugsink"): + context = Context({'url': '/admin/'}) + result = template.render(context) + self.assertEqual(result, "/bugsink/admin/") + + def test_template_filter_without_prefix(self): + """Test the with_prefix template filter with no prefix.""" + template = Template('{% load urls %}{{ url | with_prefix }}') + + with override_bugsink_settings(BASE_URL="http://localhost:8000"): + context = Context({'url': '/admin/'}) + result = template.render(context) + self.assertEqual(result, "/admin/") + + def test_template_filter_relative_url(self): + """Test the with_prefix template filter with relative URLs.""" + template = Template('{% load urls %}{{ url | with_prefix }}') + + with override_bugsink_settings(BASE_URL="https://example.com/bugsink"): + context = Context({'url': 'admin/'}) + result = template.render(context) + self.assertEqual(result, "admin/") # Should not add prefix to relative URLs \ No newline at end of file diff --git a/issues/templates/issues/base.html b/issues/templates/issues/base.html index e4d1704..d05329b 100644 --- a/issues/templates/issues/base.html +++ b/issues/templates/issues/base.html @@ -5,6 +5,7 @@ {% load stricter_templates %} {% load add_to_qs %} {% load i18n %} +{% load urls %} {% block title %}{{ issue.title }} · {{ block.super }}{% endblock %} @@ -109,13 +110,13 @@ {# overflow-x-auto is needed at the level of the flex item such that it works at the level where we need it (the code listings)#}