diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c5fb94..050a0d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: args: [--target-version, "6.0"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.8 + rev: v0.15.9 hooks: - id: ruff-check args: ["--fix", "--exit-non-zero-on-fix"] diff --git a/chzzk/management/commands/import_chzzk_campaign.py b/chzzk/management/commands/import_chzzk_campaign.py index 3ba11ed..c8944ca 100644 --- a/chzzk/management/commands/import_chzzk_campaign.py +++ b/chzzk/management/commands/import_chzzk_campaign.py @@ -1,13 +1,6 @@ from typing import TYPE_CHECKING from typing import Any -if TYPE_CHECKING: - import argparse - - from chzzk.schemas import ChzzkCampaignV2 - from chzzk.schemas import ChzzkRewardV2 - - import requests from django.core.management.base import BaseCommand from django.core.management.base import CommandError @@ -17,6 +10,12 @@ from chzzk.models import ChzzkCampaign from chzzk.models import ChzzkReward from chzzk.schemas import ChzzkApiResponseV2 +if TYPE_CHECKING: + import argparse + + from chzzk.schemas import ChzzkCampaignV2 + from chzzk.schemas import ChzzkRewardV2 + MAX_CAMPAIGN_OUTLIER_THRESHOLD: int = 100_000_000 MAX_CAMPAIGN_OUTLIER_GAP: int = 1_000 diff --git a/config/settings.py b/config/settings.py index dbb1285..a084435 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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 diff --git a/config/tests/test_seo.py b/config/tests/test_seo.py index 86f3e85..dcf5411 100644 --- a/config/tests/test_seo.py +++ b/config/tests/test_seo.py @@ -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 ( - '' - in content + canonical_pattern: re.Pattern[str] = re.compile( + r'', ) + 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 ( - '' - in content + canonical_pattern: re.Pattern[str] = re.compile( + r'', ) + assert canonical_pattern.search(content) is not None def test_meta_tags_twitter_card_is_summary_without_image() -> None: diff --git a/core/base_url.py b/core/base_url.py new file mode 100644 index 0000000..66efa86 --- /dev/null +++ b/core/base_url.py @@ -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 diff --git a/core/context_processors.py b/core/context_processors.py new file mode 100644 index 0000000..66476b8 --- /dev/null +++ b/core/context_processors.py @@ -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", ""), + } diff --git a/core/tests/test_base_url.py b/core/tests/test_base_url.py new file mode 100644 index 0000000..ee0c8c4 --- /dev/null +++ b/core/tests/test_base_url.py @@ -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" diff --git a/core/views.py b/core/views.py index 2c6e8b1..c040c4e 100644 --- a/core/views.py +++ b/core/views.py @@ -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}", ), }, diff --git a/kick/views.py b/kick/views.py index 511447f..b8cc126 100644 --- a/kick/views.py +++ b/kick/views.py @@ -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] = { diff --git a/templates/includes/meta_tags.html b/templates/includes/meta_tags.html index d90c294..8715692 100644 --- a/templates/includes/meta_tags.html +++ b/templates/includes/meta_tags.html @@ -32,7 +32,7 @@ content="{% firstof page_description 'ttvdrops - Track Twitch drops.' %}" /> + content="{% if page_url %}{{ page_url }}{% else %}{{ BASE_URL }}{{ request.get_full_path }}{% endif %}" /> {% if page_image %} {% if page_image_width and page_image_height %} @@ -41,7 +41,8 @@ {% endif %} {% endif %} {# Twitter Card tags for rich previews #} - + @@ -50,20 +51,13 @@ {% if published_date %}{% endif %} {% if modified_date %}{% endif %} {# Canonical tag #} - + {# Pagination links (for crawler efficiency) #} {% if pagination_info %} {% for link in pagination_info %}{% endfor %} {% endif %} {# Schema.org JSON-LD structured data #} -{% if schema_data %} - -{% endif %} +{% if schema_data %}{% endif %} {# Breadcrumb schema #} -{% if breadcrumb_schema %} - -{% endif %} +{% if breadcrumb_schema %}{% endif %} diff --git a/twitch/feeds.py b/twitch/feeds.py index dda8a71..b06c02b 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -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, diff --git a/twitch/views.py b/twitch/views.py index a541e29..ed1c0c7 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -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]), ), } diff --git a/youtube/views.py b/youtube/views.py index 5695dba..dd6aa18 100644 --- a/youtube/views.py +++ b/youtube/views.py @@ -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, }