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
All checks were successful
Deploy to Server / deploy (push) Successful in 22s
This commit is contained in:
parent
999ab368e2
commit
d4fd35769d
11 changed files with 250 additions and 167 deletions
|
|
@ -167,6 +167,7 @@ TEMPLATES: list[dict[str, Any]] = [
|
|||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"core.context_processors.base_url",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -263,87 +264,3 @@ CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
|
|||
BASE_URL: str = "https://ttvdrops.lovinator.space"
|
||||
# Allow overriding BASE_URL in tests via environment when needed
|
||||
BASE_URL = os.getenv("BASE_URL", BASE_URL)
|
||||
|
||||
# Monkeypatch HttpRequest.build_absolute_uri to prefer BASE_URL for absolute URLs
|
||||
try:
|
||||
from django.http.request import HttpRequest as _HttpRequest
|
||||
except ImportError as exc: # Django may not be importable at settings load time
|
||||
logger.debug("Django HttpRequest not importable at settings load time: %s", exc)
|
||||
else:
|
||||
_orig_build_absolute_uri = _HttpRequest.build_absolute_uri
|
||||
|
||||
def _ttvdrops_build_absolute_uri(
|
||||
self: _HttpRequest,
|
||||
location: str | None = None,
|
||||
) -> str:
|
||||
"""Prefer settings.BASE_URL when building absolute URIs for relative paths.
|
||||
|
||||
This makes test output deterministic (uses https://ttvdrops.lovinator.space)
|
||||
instead of Django's test client default of http://testserver.
|
||||
|
||||
Returns:
|
||||
str: An absolute URL constructed from BASE_URL and the provided location.
|
||||
"""
|
||||
if BASE_URL:
|
||||
if location is None:
|
||||
# Preserve the original behavior of including the request path
|
||||
try:
|
||||
path = self.get_full_path()
|
||||
return BASE_URL.rstrip("/") + path
|
||||
except AttributeError as exc:
|
||||
logger.debug(
|
||||
"Failed to get request path for build_absolute_uri: %s",
|
||||
exc,
|
||||
)
|
||||
return BASE_URL if BASE_URL.endswith("/") else f"{BASE_URL}/"
|
||||
if isinstance(location, str) and location.startswith("/"):
|
||||
return BASE_URL.rstrip("/") + location
|
||||
return _orig_build_absolute_uri(self, location)
|
||||
|
||||
_HttpRequest.build_absolute_uri = _ttvdrops_build_absolute_uri
|
||||
|
||||
# Ensure request.is_secure reports True so syndication.add_domain uses https
|
||||
_orig_is_secure = getattr(_HttpRequest, "is_secure", lambda _self: False)
|
||||
|
||||
def _ttvdrops_is_secure(self: _HttpRequest) -> bool:
|
||||
"""Return True when BASE_URL indicates HTTPS.
|
||||
|
||||
Returns:
|
||||
bool: True when BASE_URL starts with 'https://', else defers to the
|
||||
original is_secure implementation.
|
||||
"""
|
||||
return BASE_URL.startswith("https://")
|
||||
|
||||
_HttpRequest.is_secure = _ttvdrops_is_secure
|
||||
|
||||
# Monkeypatch django.contrib.sites.shortcuts.get_current_site to prefer BASE_URL
|
||||
try:
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from django.contrib.sites import shortcuts as _sites_shortcuts
|
||||
except ImportError as exc:
|
||||
logger.debug("Django sites.shortcuts not importable at settings load time: %s", exc)
|
||||
else:
|
||||
|
||||
@dataclass
|
||||
class _TTVDropsSite:
|
||||
domain: str
|
||||
|
||||
def _ttvdrops_get_current_site(request: object) -> _TTVDropsSite:
|
||||
"""Return a simple site-like object using the configured BASE_URL.
|
||||
|
||||
Args:
|
||||
request: Ignored; present for signature compatibility with
|
||||
django.contrib.sites.shortcuts.get_current_site.
|
||||
|
||||
Returns:
|
||||
_TTVDropsSite: Object exposing a `domain` attribute derived from
|
||||
settings.BASE_URL.
|
||||
"""
|
||||
parts = urlsplit(BASE_URL)
|
||||
domain = parts.netloc or parts.path
|
||||
return _TTVDropsSite(domain=domain)
|
||||
|
||||
_sites_shortcuts.get_current_site = _ttvdrops_get_current_site
|
||||
|
|
|
|||
|
|
@ -52,10 +52,10 @@ def test_meta_tags_use_request_absolute_url_for_og_url_and_canonical() -> None:
|
|||
_extract_meta_content(content, "og:url")
|
||||
== "https://ttvdrops.lovinator.space/drops/"
|
||||
)
|
||||
assert (
|
||||
'<link rel="canonical" href="https://ttvdrops.lovinator.space/drops/" />'
|
||||
in content
|
||||
canonical_pattern: re.Pattern[str] = re.compile(
|
||||
r'<link\s+rel="canonical"\s+href="https://ttvdrops\.lovinator\.space/drops/"\s*/?>',
|
||||
)
|
||||
assert canonical_pattern.search(content) is not None
|
||||
|
||||
|
||||
def test_meta_tags_use_explicit_page_url_for_og_url_and_canonical() -> None:
|
||||
|
|
@ -71,10 +71,10 @@ def test_meta_tags_use_explicit_page_url_for_og_url_and_canonical() -> None:
|
|||
_extract_meta_content(content, "og:url")
|
||||
== "https://ttvdrops.lovinator.space/custom-page/"
|
||||
)
|
||||
assert (
|
||||
'<link rel="canonical" href="https://ttvdrops.lovinator.space/custom-page/" />'
|
||||
in content
|
||||
canonical_pattern: re.Pattern[str] = re.compile(
|
||||
r'<link\s+rel="canonical"\s+href="https://ttvdrops\.lovinator\.space/custom-page/"\s*/?>',
|
||||
)
|
||||
assert canonical_pattern.search(content) is not None
|
||||
|
||||
|
||||
def test_meta_tags_twitter_card_is_summary_without_image() -> None:
|
||||
|
|
|
|||
80
core/base_url.py
Normal file
80
core/base_url.py
Normal 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
|
||||
17
core/context_processors.py
Normal file
17
core/context_processors.py
Normal 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", ""),
|
||||
}
|
||||
25
core/tests/test_base_url.py
Normal file
25
core/tests/test_base_url.py
Normal 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"
|
||||
|
|
@ -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}",
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from django.shortcuts import render
|
|||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from core.base_url import build_absolute_uri
|
||||
from kick.models import KickCategory
|
||||
from kick.models import KickDropCampaign
|
||||
from kick.models import KickOrganization
|
||||
|
|
@ -127,14 +128,14 @@ def _build_pagination_info(
|
|||
if page_obj.has_previous():
|
||||
links.append({
|
||||
"rel": "prev",
|
||||
"url": request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
f"{base_url}{sep}page={page_obj.previous_page_number()}",
|
||||
),
|
||||
})
|
||||
if page_obj.has_next():
|
||||
links.append({
|
||||
"rel": "next",
|
||||
"url": request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
f"{base_url}{sep}page={page_obj.next_page_number()}",
|
||||
),
|
||||
})
|
||||
|
|
@ -239,7 +240,7 @@ def campaign_list_view(request: HttpRequest) -> HttpResponse:
|
|||
seo_context: dict[str, str] = _build_seo_context(
|
||||
page_title=title,
|
||||
page_description="Browse Kick drop campaigns.",
|
||||
seo_meta={"page_url": request.build_absolute_uri(base_url)},
|
||||
seo_meta={"page_url": build_absolute_uri(base_url)},
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
|
|
@ -291,20 +292,20 @@ def campaign_detail_view(request: HttpRequest, kick_id: str) -> HttpResponse:
|
|||
channels: list[KickChannel] = list(campaign.channels.select_related("user"))
|
||||
|
||||
breadcrumb_schema: str = _build_breadcrumb_schema([
|
||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||
{"name": "Home", "url": build_absolute_uri("/")},
|
||||
{
|
||||
"name": "Kick Campaigns",
|
||||
"url": request.build_absolute_uri(reverse("kick:campaign_list")),
|
||||
"url": build_absolute_uri(reverse("kick:campaign_list")),
|
||||
},
|
||||
{
|
||||
"name": campaign.name,
|
||||
"url": request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse("kick:campaign_detail", args=[campaign.kick_id]),
|
||||
),
|
||||
},
|
||||
])
|
||||
|
||||
campaign_url: str = request.build_absolute_uri(
|
||||
campaign_url: str = build_absolute_uri(
|
||||
reverse("kick:campaign_detail", args=[campaign.kick_id]),
|
||||
)
|
||||
campaign_event: dict[str, Any] = {
|
||||
|
|
@ -431,20 +432,20 @@ def category_detail_view(request: HttpRequest, kick_id: int) -> HttpResponse:
|
|||
]
|
||||
|
||||
breadcrumb_schema: str = _build_breadcrumb_schema([
|
||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||
{"name": "Home", "url": build_absolute_uri("/")},
|
||||
{
|
||||
"name": "Kick Games",
|
||||
"url": request.build_absolute_uri(reverse("kick:game_list")),
|
||||
"url": build_absolute_uri(reverse("kick:game_list")),
|
||||
},
|
||||
{
|
||||
"name": category.name,
|
||||
"url": request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse("kick:game_detail", args=[category.kick_id]),
|
||||
),
|
||||
},
|
||||
])
|
||||
|
||||
category_url: str = request.build_absolute_uri(
|
||||
category_url: str = build_absolute_uri(
|
||||
reverse("kick:game_detail", args=[category.kick_id]),
|
||||
)
|
||||
category_schema: dict[str, Any] = {
|
||||
|
|
@ -536,20 +537,20 @@ def organization_detail_view(request: HttpRequest, kick_id: str) -> HttpResponse
|
|||
)
|
||||
|
||||
breadcrumb_schema: str = _build_breadcrumb_schema([
|
||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||
{"name": "Home", "url": build_absolute_uri("/")},
|
||||
{
|
||||
"name": "Kick Organizations",
|
||||
"url": request.build_absolute_uri(reverse("kick:organization_list")),
|
||||
"url": build_absolute_uri(reverse("kick:organization_list")),
|
||||
},
|
||||
{
|
||||
"name": org.name,
|
||||
"url": request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse("kick:organization_detail", args=[org.kick_id]),
|
||||
),
|
||||
},
|
||||
])
|
||||
|
||||
org_url: str = request.build_absolute_uri(
|
||||
org_url: str = build_absolute_uri(
|
||||
reverse("kick:organization_detail", args=[org.kick_id]),
|
||||
)
|
||||
organization_node: dict[str, Any] = {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
content="{% firstof page_description 'ttvdrops - Track Twitch drops.' %}" />
|
||||
<meta property="og:type" content="{% firstof og_type 'website' %}" />
|
||||
<meta property="og:url"
|
||||
content="{% firstof page_url request.build_absolute_uri %}" />
|
||||
content="{% if page_url %}{{ page_url }}{% else %}{{ BASE_URL }}{{ request.get_full_path }}{% endif %}" />
|
||||
{% if page_image %}
|
||||
<meta property="og:image" content="{{ page_image }}" />
|
||||
{% if page_image_width and page_image_height %}
|
||||
|
|
@ -41,7 +41,8 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{# Twitter Card tags for rich previews #}
|
||||
<meta name="twitter:card" content="{% if page_image %}summary_large_image{% else %}summary{% endif %}" />
|
||||
<meta name="twitter:card"
|
||||
content="{% if page_image %}summary_large_image{% else %}summary{% endif %}" />
|
||||
<meta name="twitter:title" content="{% firstof page_title 'ttvdrops' %}" />
|
||||
<meta name="twitter:description"
|
||||
content="{% firstof page_description 'ttvdrops - Twitch and Kick drops.' %}" />
|
||||
|
|
@ -50,20 +51,13 @@
|
|||
{% if published_date %}<meta property="article:published_time" content="{{ published_date }}" />{% endif %}
|
||||
{% if modified_date %}<meta property="article:modified_time" content="{{ modified_date }}" />{% endif %}
|
||||
{# Canonical tag #}
|
||||
<link rel="canonical" href="{% firstof page_url request.build_absolute_uri %}" />
|
||||
<link rel="canonical"
|
||||
href="{% if page_url %}{{ page_url }}{% else %}{{ BASE_URL }}{{ request.get_full_path }}{% endif %}" />
|
||||
{# Pagination links (for crawler efficiency) #}
|
||||
{% if pagination_info %}
|
||||
{% for link in pagination_info %}<link rel="{{ link.rel }}" href="{{ link.url }}" />{% endfor %}
|
||||
{% endif %}
|
||||
{# Schema.org JSON-LD structured data #}
|
||||
{% if schema_data %}
|
||||
<script type="application/ld+json">
|
||||
{{ schema_data|safe }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% if schema_data %}<script type="application/ld+json">{{ schema_data|safe }}</script>{% endif %}
|
||||
{# Breadcrumb schema #}
|
||||
{% if breadcrumb_schema %}
|
||||
<script type="application/ld+json">
|
||||
{{ breadcrumb_schema|safe }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% if breadcrumb_schema %}<script type="application/ld+json">{{ breadcrumb_schema|safe }}</script>{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import logging
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Literal
|
||||
|
||||
import django.contrib.syndication.views as syndication_views
|
||||
from django.conf import settings
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models.query import QuerySet
|
||||
|
|
@ -16,6 +20,8 @@ from django.utils.html import format_html
|
|||
from django.utils.html import format_html_join
|
||||
from django.utils.safestring import SafeText
|
||||
|
||||
from core.base_url import build_absolute_uri
|
||||
from core.base_url import get_current_site # noqa: F811
|
||||
from twitch.models import Channel
|
||||
from twitch.models import ChatBadge
|
||||
from twitch.models import DropCampaign
|
||||
|
|
@ -26,11 +32,15 @@ from twitch.models import TimeBasedDrop
|
|||
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
from collections.abc import Callable
|
||||
|
||||
from django.contrib.sites.models import Site
|
||||
from django.contrib.sites.requests import RequestSite
|
||||
from django.db.models import Model
|
||||
from django.db.models import QuerySet
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpResponse
|
||||
from django.utils.feedgenerator import SyndicationFeed
|
||||
from django.utils.safestring import SafeString
|
||||
|
||||
from twitch.models import DropBenefit
|
||||
|
|
@ -99,14 +109,14 @@ class TTVDropsBaseFeed(Feed):
|
|||
"""
|
||||
if self._request is None:
|
||||
return url
|
||||
return self._request.build_absolute_uri(url)
|
||||
return build_absolute_uri(url, request=self._request)
|
||||
|
||||
def _absolute_stylesheet_urls(self, request: HttpRequest) -> list[str]:
|
||||
"""Return stylesheet URLs as absolute HTTP URLs for browser/file compatibility."""
|
||||
return [
|
||||
href
|
||||
if href.startswith(("http://", "https://"))
|
||||
else request.build_absolute_uri(href)
|
||||
else build_absolute_uri(href, request=request)
|
||||
for href in self.stylesheets
|
||||
]
|
||||
|
||||
|
|
@ -151,6 +161,41 @@ class TTVDropsBaseFeed(Feed):
|
|||
|
||||
response.content = content.encode(encoding)
|
||||
|
||||
def get_feed(self, obj: object, request: HttpRequest) -> SyndicationFeed:
|
||||
"""Use deterministic BASE_URL handling for syndication feed generation.
|
||||
|
||||
Returns:
|
||||
SyndicationFeed: The feed generator instance with the correct site and URL context for absolute URL generation.
|
||||
"""
|
||||
# TODO(TheLovinator): Refactor to avoid this mess. # noqa: TD003
|
||||
try:
|
||||
from django.contrib.sites import shortcuts as sites_shortcuts # noqa: I001, PLC0415
|
||||
except ImportError:
|
||||
sites_shortcuts = None
|
||||
|
||||
original_get_current_site: Callable[..., Site | RequestSite] | None = (
|
||||
sites_shortcuts.get_current_site if sites_shortcuts else None
|
||||
)
|
||||
original_is_secure: Callable[[], bool] = request.is_secure
|
||||
|
||||
if sites_shortcuts is not None:
|
||||
sites_shortcuts.get_current_site = get_current_site
|
||||
|
||||
original_syndication_get_current_site: (
|
||||
Callable[..., Site | RequestSite] | None
|
||||
) = syndication_views.get_current_site # pyright: ignore[reportAttributeAccessIssue]
|
||||
syndication_views.get_current_site = get_current_site # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
request.is_secure = lambda: settings.BASE_URL.startswith("https://")
|
||||
|
||||
try:
|
||||
return super().get_feed(obj, request)
|
||||
finally:
|
||||
if sites_shortcuts is not None and original_get_current_site is not None:
|
||||
sites_shortcuts.get_current_site = original_get_current_site
|
||||
syndication_views.get_current_site = original_syndication_get_current_site # pyright: ignore[reportAttributeAccessIssue]
|
||||
request.is_secure = original_is_secure
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from django.utils import timezone
|
|||
from django.views.generic import DetailView
|
||||
from django.views.generic import ListView
|
||||
|
||||
from core.base_url import build_absolute_uri
|
||||
from twitch.models import Channel
|
||||
from twitch.models import ChatBadge
|
||||
from twitch.models import ChatBadgeSet
|
||||
|
|
@ -99,7 +100,7 @@ def _build_image_object(
|
|||
|
||||
return {
|
||||
"@type": "ImageObject",
|
||||
"contentUrl": request.build_absolute_uri(image_url),
|
||||
"contentUrl": build_absolute_uri(image_url),
|
||||
"creditText": creator_name,
|
||||
"copyrightNotice": copyright_notice or creator_name,
|
||||
"creator": creator,
|
||||
|
|
@ -241,7 +242,7 @@ def _build_pagination_info(
|
|||
prev_url = f"{base_url}&page={page_obj.previous_page_number()}"
|
||||
pagination_links.append({
|
||||
"rel": "prev",
|
||||
"url": request.build_absolute_uri(prev_url),
|
||||
"url": build_absolute_uri(prev_url),
|
||||
})
|
||||
|
||||
if page_obj.has_next():
|
||||
|
|
@ -251,7 +252,7 @@ def _build_pagination_info(
|
|||
next_url = f"{base_url}&page={page_obj.next_page_number()}"
|
||||
pagination_links.append({
|
||||
"rel": "next",
|
||||
"url": request.build_absolute_uri(next_url),
|
||||
"url": build_absolute_uri(next_url),
|
||||
})
|
||||
|
||||
return pagination_links or None
|
||||
|
|
@ -320,7 +321,7 @@ def org_list_view(request: HttpRequest) -> HttpResponse:
|
|||
"@type": "CollectionPage",
|
||||
"name": "Twitch Organizations",
|
||||
"description": "List of Twitch organizations.",
|
||||
"url": request.build_absolute_uri("/organizations/"),
|
||||
"url": build_absolute_uri("/organizations/"),
|
||||
}
|
||||
|
||||
seo_context: dict[str, Any] = _build_seo_context(
|
||||
|
|
@ -363,7 +364,7 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon
|
|||
s: Literal["", "s"] = "" if games_count == 1 else "s"
|
||||
org_description: str = f"{org_name} has {games_count} game{s}."
|
||||
|
||||
url: str = request.build_absolute_uri(
|
||||
url: str = build_absolute_uri(
|
||||
reverse("twitch:organization_detail", args=[organization.twitch_id]),
|
||||
)
|
||||
organization_node: dict[str, Any] = {
|
||||
|
|
@ -388,11 +389,11 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon
|
|||
|
||||
# Breadcrumb schema
|
||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||
{"name": "Organizations", "url": request.build_absolute_uri("/organizations/")},
|
||||
{"name": "Home", "url": build_absolute_uri("/")},
|
||||
{"name": "Organizations", "url": build_absolute_uri("/organizations/")},
|
||||
{
|
||||
"name": org_name,
|
||||
"url": request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse("twitch:organization_detail", args=[organization.twitch_id]),
|
||||
),
|
||||
},
|
||||
|
|
@ -496,14 +497,14 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0
|
|||
"@type": "CollectionPage",
|
||||
"name": title,
|
||||
"description": description,
|
||||
"url": request.build_absolute_uri(base_url),
|
||||
"url": build_absolute_uri(base_url),
|
||||
}
|
||||
|
||||
seo_context: dict[str, Any] = _build_seo_context(
|
||||
page_title=title,
|
||||
page_description=description,
|
||||
seo_meta={
|
||||
"page_url": request.build_absolute_uri(base_url),
|
||||
"page_url": build_absolute_uri(base_url),
|
||||
"pagination_info": pagination_info,
|
||||
"schema_data": collection_schema,
|
||||
},
|
||||
|
|
@ -636,7 +637,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
|||
campaign.image_height if campaign.image_file else None
|
||||
)
|
||||
|
||||
url: str = request.build_absolute_uri(
|
||||
url: str = build_absolute_uri(
|
||||
reverse("twitch:campaign_detail", args=[campaign.twitch_id]),
|
||||
)
|
||||
|
||||
|
|
@ -667,7 +668,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
|||
else "Twitch"
|
||||
)
|
||||
campaign_owner_url: str = (
|
||||
request.build_absolute_uri(
|
||||
build_absolute_uri(
|
||||
reverse("twitch:organization_detail", args=[campaign_owner.twitch_id]),
|
||||
)
|
||||
if campaign_owner
|
||||
|
|
@ -701,17 +702,17 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
|||
campaign.game.display_name or campaign.game.name or campaign.game.twitch_id
|
||||
)
|
||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||
{"name": "Games", "url": request.build_absolute_uri("/games/")},
|
||||
{"name": "Home", "url": build_absolute_uri("/")},
|
||||
{"name": "Games", "url": build_absolute_uri("/games/")},
|
||||
{
|
||||
"name": game_name,
|
||||
"url": request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse("twitch:game_detail", args=[campaign.game.twitch_id]),
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": campaign_name,
|
||||
"url": request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse("twitch:campaign_detail", args=[campaign.twitch_id]),
|
||||
),
|
||||
},
|
||||
|
|
@ -821,7 +822,7 @@ class GamesGridView(ListView):
|
|||
"@type": "CollectionPage",
|
||||
"name": "Twitch Games",
|
||||
"description": "Twitch games that had or have Twitch drops.",
|
||||
"url": self.request.build_absolute_uri("/games/"),
|
||||
"url": build_absolute_uri("/games/"),
|
||||
}
|
||||
|
||||
seo_context: dict[str, Any] = _build_seo_context(
|
||||
|
|
@ -977,7 +978,7 @@ class GameDetailView(DetailView):
|
|||
"@type": "VideoGame",
|
||||
"name": game_name,
|
||||
"description": game_description,
|
||||
"url": self.request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse("twitch:game_detail", args=[game.twitch_id]),
|
||||
),
|
||||
}
|
||||
|
|
@ -992,7 +993,7 @@ class GameDetailView(DetailView):
|
|||
else "Twitch"
|
||||
)
|
||||
owner_url: str = (
|
||||
self.request.build_absolute_uri(
|
||||
build_absolute_uri(
|
||||
reverse("twitch:organization_detail", args=[preferred_owner.twitch_id]),
|
||||
)
|
||||
if preferred_owner
|
||||
|
|
@ -1014,11 +1015,11 @@ class GameDetailView(DetailView):
|
|||
|
||||
# Breadcrumb schema
|
||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||
{"name": "Home", "url": self.request.build_absolute_uri("/")},
|
||||
{"name": "Games", "url": self.request.build_absolute_uri("/games/")},
|
||||
{"name": "Home", "url": build_absolute_uri("/")},
|
||||
{"name": "Games", "url": build_absolute_uri("/games/")},
|
||||
{
|
||||
"name": game_name,
|
||||
"url": self.request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse("twitch:game_detail", args=[game.twitch_id]),
|
||||
),
|
||||
},
|
||||
|
|
@ -1114,12 +1115,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}",
|
||||
),
|
||||
},
|
||||
|
|
@ -1219,14 +1220,14 @@ def reward_campaign_list_view(request: HttpRequest) -> HttpResponse:
|
|||
"@type": "CollectionPage",
|
||||
"name": title,
|
||||
"description": description,
|
||||
"url": request.build_absolute_uri(base_url),
|
||||
"url": build_absolute_uri(base_url),
|
||||
}
|
||||
|
||||
seo_context: dict[str, Any] = _build_seo_context(
|
||||
page_title=title,
|
||||
page_description=description,
|
||||
seo_meta={
|
||||
"page_url": request.build_absolute_uri(base_url),
|
||||
"page_url": build_absolute_uri(base_url),
|
||||
"pagination_info": pagination_info,
|
||||
"schema_data": collection_schema,
|
||||
},
|
||||
|
|
@ -1275,7 +1276,7 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes
|
|||
else f"{campaign_name}"
|
||||
)
|
||||
|
||||
reward_url: str = request.build_absolute_uri(
|
||||
reward_url: str = build_absolute_uri(
|
||||
reverse("twitch:reward_campaign_detail", args=[reward_campaign.twitch_id]),
|
||||
)
|
||||
|
||||
|
|
@ -1316,14 +1317,14 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes
|
|||
|
||||
# Breadcrumb schema
|
||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||
{"name": "Home", "url": build_absolute_uri("/")},
|
||||
{
|
||||
"name": "Reward Campaigns",
|
||||
"url": request.build_absolute_uri("/reward-campaigns/"),
|
||||
"url": build_absolute_uri("/reward-campaigns/"),
|
||||
},
|
||||
{
|
||||
"name": campaign_name,
|
||||
"url": request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse(
|
||||
"twitch:reward_campaign_detail",
|
||||
args=[reward_campaign.twitch_id],
|
||||
|
|
@ -1418,14 +1419,14 @@ class ChannelListView(ListView):
|
|||
"@type": "CollectionPage",
|
||||
"name": "Twitch Channels",
|
||||
"description": "List of Twitch channels participating in drop campaigns.",
|
||||
"url": self.request.build_absolute_uri("/channels/"),
|
||||
"url": build_absolute_uri("/channels/"),
|
||||
}
|
||||
|
||||
seo_context: dict[str, Any] = _build_seo_context(
|
||||
page_title="Twitch Channels",
|
||||
page_description="List of Twitch channels participating in drop campaigns.",
|
||||
seo_meta={
|
||||
"page_url": self.request.build_absolute_uri(base_url),
|
||||
"page_url": build_absolute_uri(base_url),
|
||||
"pagination_info": pagination_info,
|
||||
"schema_data": collection_schema,
|
||||
},
|
||||
|
|
@ -1542,7 +1543,7 @@ class ChannelDetailView(DetailView):
|
|||
if total_campaigns > 1:
|
||||
description += "s"
|
||||
|
||||
channel_url: str = self.request.build_absolute_uri(
|
||||
channel_url: str = build_absolute_uri(
|
||||
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
||||
)
|
||||
channel_node: dict[str, Any] = {
|
||||
|
|
@ -1569,11 +1570,11 @@ class ChannelDetailView(DetailView):
|
|||
|
||||
# Breadcrumb schema
|
||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||
{"name": "Home", "url": self.request.build_absolute_uri("/")},
|
||||
{"name": "Channels", "url": self.request.build_absolute_uri("/channels/")},
|
||||
{"name": "Home", "url": build_absolute_uri("/")},
|
||||
{"name": "Channels", "url": build_absolute_uri("/channels/")},
|
||||
{
|
||||
"name": name,
|
||||
"url": self.request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
||||
),
|
||||
},
|
||||
|
|
@ -1638,7 +1639,7 @@ def badge_list_view(request: HttpRequest) -> HttpResponse:
|
|||
"@type": "CollectionPage",
|
||||
"name": "Twitch chat badges",
|
||||
"description": "List of Twitch chat badges awarded through drop campaigns.",
|
||||
"url": request.build_absolute_uri("/badges/"),
|
||||
"url": build_absolute_uri("/badges/"),
|
||||
}
|
||||
|
||||
seo_context: dict[str, Any] = _build_seo_context(
|
||||
|
|
@ -1722,7 +1723,7 @@ def badge_set_detail_view(request: HttpRequest, set_id: str) -> HttpResponse:
|
|||
"@type": "ItemList",
|
||||
"name": badge_set_name,
|
||||
"description": badge_set_description,
|
||||
"url": request.build_absolute_uri(
|
||||
"url": build_absolute_uri(
|
||||
reverse("twitch:badge_set_detail", args=[badge_set.set_id]),
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from typing import Any
|
|||
|
||||
from django.shortcuts import render
|
||||
|
||||
from core.base_url import build_absolute_uri
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpResponse
|
||||
|
|
@ -136,7 +138,7 @@ def index(request: HttpRequest) -> HttpResponse:
|
|||
"@type": "ItemList",
|
||||
"name": PAGE_TITLE,
|
||||
"description": PAGE_DESCRIPTION,
|
||||
"url": request.build_absolute_uri(),
|
||||
"url": build_absolute_uri(request=request),
|
||||
"itemListElement": list_items,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue