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

View file

@ -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,

View file

@ -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]),
),
}