Refactor URL handling to use BASE_URL across the application and add base_url context processor
All checks were successful
Deploy to Server / deploy (push) Successful in 22s

This commit is contained in:
Joakim Hellsén 2026-04-03 19:51:01 +02:00
commit d4fd35769d
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
11 changed files with 250 additions and 167 deletions

80
core/base_url.py Normal file
View file

@ -0,0 +1,80 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from urllib.parse import urlsplit
from django.conf import settings
if TYPE_CHECKING:
from django.http import HttpRequest
def _get_base_url() -> str:
"""Get normalized BASE_URL from settings.
Returns:
str: The configured BASE_URL without trailing slash.
"""
base_url = getattr(settings, "BASE_URL", "")
return base_url.rstrip("/") if base_url else ""
def build_absolute_uri(
location: str | None = None,
request: HttpRequest | None = None,
) -> str:
"""Build an absolute URI via BASE_URL (preferred) or request fallback.
Args:
location: Relative path ('/foo/') or absolute URL.
request: Optional HttpRequest to resolve path when location is None.
Returns:
str: Fully resolved absolute URL.
"""
base_url = _get_base_url()
if location is None:
if request is not None:
location = request.get_full_path()
else:
return f"{base_url}/" if base_url else "/"
parsed = urlsplit(location)
if parsed.scheme and parsed.netloc:
return location
if base_url:
if location.startswith("/"):
return f"{base_url}{location}"
return f"{base_url}/{location.lstrip('/')}"
if request is not None:
return request.build_absolute_uri(location)
return location
def is_secure() -> bool:
"""Return whether the configured BASE_URL uses HTTPS."""
base_url = _get_base_url()
return base_url.startswith("https://") if base_url else False
@dataclass
class _TTVDropsSite:
domain: str
def get_current_site(request: object) -> _TTVDropsSite:
"""Return a site-like object with domain derived from BASE_URL."""
base_url = _get_base_url()
parts = urlsplit(base_url)
domain = parts.netloc or parts.path
return _TTVDropsSite(domain=domain)
def apply_base_url_patches() -> None:
"""No-op; use build_absolute_uri() helper explicitly."""
return

View file

@ -0,0 +1,17 @@
from typing import TYPE_CHECKING
from django.conf import settings
if TYPE_CHECKING:
from django.http import HttpRequest
def base_url(request: HttpRequest) -> dict[str, str]:
"""Provide BASE_URL to templates for deterministic absolute URL creation.
Returns:
dict[str, str]: A dictionary containing the BASE_URL.
"""
return {
"BASE_URL": getattr(settings, "BASE_URL", ""),
}

View file

@ -0,0 +1,25 @@
from typing import TYPE_CHECKING
from django.test import RequestFactory
from core.base_url import build_absolute_uri
from core.base_url import get_current_site
if TYPE_CHECKING:
from config.tests.test_seo import WSGIRequest
from core.base_url import _TTVDropsSite
def test_build_absolute_uri_uses_base_url() -> None:
"""Test that build_absolute_uri uses the base URL from settings."""
request: WSGIRequest = RequestFactory().get("/test-path/")
assert (
build_absolute_uri(request=request)
== "https://ttvdrops.lovinator.space/test-path/"
)
def test_get_current_site_from_base_url() -> None:
"""Test that get_current_site returns the correct site based on the base URL."""
site: _TTVDropsSite = get_current_site(None)
assert site.domain == "ttvdrops.lovinator.space"

View file

@ -26,6 +26,7 @@ from django.template.defaultfilters import filesizeformat
from django.urls import reverse
from django.utils import timezone
from core.base_url import build_absolute_uri
from kick.models import KickChannel
from kick.models import KickDropCampaign
from twitch.models import Channel
@ -507,7 +508,7 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
seo_context: dict[str, Any] = _build_seo_context(
page_title="Feed Documentation",
page_description="Documentation for the RSS feeds available on ttvdrops.lovinator.space, including how to use them and what data they contain.",
page_url=request.build_absolute_uri(reverse("core:docs_rss")),
page_url=build_absolute_uri(reverse("core:docs_rss")),
)
return render(
@ -721,7 +722,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
dataset_distributions.append({
"@type": "DataDownload",
"name": dataset["name"],
"contentUrl": request.build_absolute_uri(
"contentUrl": build_absolute_uri(
reverse("core:dataset_backup_download", args=[download_path]),
),
"encodingFormat": "application/zstd",
@ -731,9 +732,9 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
"@context": "https://schema.org",
"@type": "Dataset",
"name": "Historical archive of Twitch and Kick drop data",
"identifier": request.build_absolute_uri(reverse("core:dataset_backups")),
"identifier": build_absolute_uri(reverse("core:dataset_backups")),
"temporalCoverage": "2024-07-17/..",
"url": request.build_absolute_uri(reverse("core:dataset_backups")),
"url": build_absolute_uri(reverse("core:dataset_backups")),
"license": "https://creativecommons.org/publicdomain/zero/1.0/",
"isAccessibleForFree": True,
"description": (
@ -753,7 +754,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
"includedInDataCatalog": {
"@type": "DataCatalog",
"name": "ttvdrops.lovinator.space",
"url": request.build_absolute_uri(reverse("core:dataset_backups")),
"url": build_absolute_uri(reverse("core:dataset_backups")),
},
}
if dataset_distributions:
@ -905,7 +906,7 @@ def search_view(request: HttpRequest) -> HttpResponse:
seo_context: dict[str, Any] = _build_seo_context(
page_title=page_title,
page_description=page_description,
page_url=request.build_absolute_uri(reverse("core:search")),
page_url=build_absolute_uri(reverse("core:search")),
)
return render(
request,
@ -1006,12 +1007,12 @@ def dashboard(request: HttpRequest) -> HttpResponse:
"@context": "https://schema.org",
"@type": "WebSite",
"name": "ttvdrops",
"url": request.build_absolute_uri("/"),
"url": build_absolute_uri("/"),
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": request.build_absolute_uri(
"urlTemplate": build_absolute_uri(
"/search/?q={search_term_string}",
),
},