diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 050a0d8..2c5fb94 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.9 + rev: v0.15.8 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 c8944ca..3ba11ed 100644 --- a/chzzk/management/commands/import_chzzk_campaign.py +++ b/chzzk/management/commands/import_chzzk_campaign.py @@ -1,6 +1,13 @@ 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 @@ -10,12 +17,6 @@ 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 a084435..dbb1285 100644 --- a/config/settings.py +++ b/config/settings.py @@ -167,7 +167,6 @@ TEMPLATES: list[dict[str, Any]] = [ "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", - "core.context_processors.base_url", ], }, }, @@ -264,3 +263,87 @@ 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 dcf5411..86f3e85 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/" ) - canonical_pattern: re.Pattern[str] = re.compile( - r'', + assert ( + '' + in content ) - 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/" ) - canonical_pattern: re.Pattern[str] = re.compile( - r'', + assert ( + '' + in content ) - 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 deleted file mode 100644 index 66efa86..0000000 --- a/core/base_url.py +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index 66476b8..0000000 --- a/core/context_processors.py +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index ee0c8c4..0000000 --- a/core/tests/test_base_url.py +++ /dev/null @@ -1,25 +0,0 @@ -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 c040c4e..2c6e8b1 100644 --- a/core/views.py +++ b/core/views.py @@ -26,7 +26,6 @@ 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 @@ -508,7 +507,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=build_absolute_uri(reverse("core:docs_rss")), + page_url=request.build_absolute_uri(reverse("core:docs_rss")), ) return render( @@ -722,7 +721,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse: dataset_distributions.append({ "@type": "DataDownload", "name": dataset["name"], - "contentUrl": build_absolute_uri( + "contentUrl": request.build_absolute_uri( reverse("core:dataset_backup_download", args=[download_path]), ), "encodingFormat": "application/zstd", @@ -732,9 +731,9 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse: "@context": "https://schema.org", "@type": "Dataset", "name": "Historical archive of Twitch and Kick drop data", - "identifier": build_absolute_uri(reverse("core:dataset_backups")), + "identifier": request.build_absolute_uri(reverse("core:dataset_backups")), "temporalCoverage": "2024-07-17/..", - "url": build_absolute_uri(reverse("core:dataset_backups")), + "url": request.build_absolute_uri(reverse("core:dataset_backups")), "license": "https://creativecommons.org/publicdomain/zero/1.0/", "isAccessibleForFree": True, "description": ( @@ -754,7 +753,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse: "includedInDataCatalog": { "@type": "DataCatalog", "name": "ttvdrops.lovinator.space", - "url": build_absolute_uri(reverse("core:dataset_backups")), + "url": request.build_absolute_uri(reverse("core:dataset_backups")), }, } if dataset_distributions: @@ -906,7 +905,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=build_absolute_uri(reverse("core:search")), + page_url=request.build_absolute_uri(reverse("core:search")), ) return render( request, @@ -1007,12 +1006,12 @@ def dashboard(request: HttpRequest) -> HttpResponse: "@context": "https://schema.org", "@type": "WebSite", "name": "ttvdrops", - "url": build_absolute_uri("/"), + "url": request.build_absolute_uri("/"), "potentialAction": { "@type": "SearchAction", "target": { "@type": "EntryPoint", - "urlTemplate": build_absolute_uri( + "urlTemplate": request.build_absolute_uri( "/search/?q={search_term_string}", ), }, diff --git a/kick/views.py b/kick/views.py index b8cc126..511447f 100644 --- a/kick/views.py +++ b/kick/views.py @@ -15,7 +15,6 @@ 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 @@ -128,14 +127,14 @@ def _build_pagination_info( if page_obj.has_previous(): links.append({ "rel": "prev", - "url": build_absolute_uri( + "url": request.build_absolute_uri( f"{base_url}{sep}page={page_obj.previous_page_number()}", ), }) if page_obj.has_next(): links.append({ "rel": "next", - "url": build_absolute_uri( + "url": request.build_absolute_uri( f"{base_url}{sep}page={page_obj.next_page_number()}", ), }) @@ -240,7 +239,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": build_absolute_uri(base_url)}, + seo_meta={"page_url": request.build_absolute_uri(base_url)}, ) return render( request, @@ -292,20 +291,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": build_absolute_uri("/")}, + {"name": "Home", "url": request.build_absolute_uri("/")}, { "name": "Kick Campaigns", - "url": build_absolute_uri(reverse("kick:campaign_list")), + "url": request.build_absolute_uri(reverse("kick:campaign_list")), }, { "name": campaign.name, - "url": build_absolute_uri( + "url": request.build_absolute_uri( reverse("kick:campaign_detail", args=[campaign.kick_id]), ), }, ]) - campaign_url: str = build_absolute_uri( + campaign_url: str = request.build_absolute_uri( reverse("kick:campaign_detail", args=[campaign.kick_id]), ) campaign_event: dict[str, Any] = { @@ -432,20 +431,20 @@ def category_detail_view(request: HttpRequest, kick_id: int) -> HttpResponse: ] breadcrumb_schema: str = _build_breadcrumb_schema([ - {"name": "Home", "url": build_absolute_uri("/")}, + {"name": "Home", "url": request.build_absolute_uri("/")}, { "name": "Kick Games", - "url": build_absolute_uri(reverse("kick:game_list")), + "url": request.build_absolute_uri(reverse("kick:game_list")), }, { "name": category.name, - "url": build_absolute_uri( + "url": request.build_absolute_uri( reverse("kick:game_detail", args=[category.kick_id]), ), }, ]) - category_url: str = build_absolute_uri( + category_url: str = request.build_absolute_uri( reverse("kick:game_detail", args=[category.kick_id]), ) category_schema: dict[str, Any] = { @@ -537,20 +536,20 @@ def organization_detail_view(request: HttpRequest, kick_id: str) -> HttpResponse ) breadcrumb_schema: str = _build_breadcrumb_schema([ - {"name": "Home", "url": build_absolute_uri("/")}, + {"name": "Home", "url": request.build_absolute_uri("/")}, { "name": "Kick Organizations", - "url": build_absolute_uri(reverse("kick:organization_list")), + "url": request.build_absolute_uri(reverse("kick:organization_list")), }, { "name": org.name, - "url": build_absolute_uri( + "url": request.build_absolute_uri( reverse("kick:organization_detail", args=[org.kick_id]), ), }, ]) - org_url: str = build_absolute_uri( + org_url: str = request.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 8715692..d90c294 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="{% firstof page_url request.build_absolute_uri %}" /> {% if page_image %} {% if page_image_width and page_image_height %} @@ -41,8 +41,7 @@ {% endif %} {% endif %} {# Twitter Card tags for rich previews #} - + @@ -51,13 +50,20 @@ {% 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 b06c02b..dda8a71 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -1,13 +1,9 @@ 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 @@ -20,8 +16,6 @@ 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 @@ -32,15 +26,11 @@ 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 @@ -109,14 +99,14 @@ class TTVDropsBaseFeed(Feed): """ if self._request is None: return url - return build_absolute_uri(url, request=self._request) + return self._request.build_absolute_uri(url) 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 build_absolute_uri(href, request=request) + else request.build_absolute_uri(href) for href in self.stylesheets ] @@ -161,41 +151,6 @@ 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 ed1c0c7..a541e29 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -28,7 +28,6 @@ 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 @@ -100,7 +99,7 @@ def _build_image_object( return { "@type": "ImageObject", - "contentUrl": build_absolute_uri(image_url), + "contentUrl": request.build_absolute_uri(image_url), "creditText": creator_name, "copyrightNotice": copyright_notice or creator_name, "creator": creator, @@ -242,7 +241,7 @@ def _build_pagination_info( prev_url = f"{base_url}&page={page_obj.previous_page_number()}" pagination_links.append({ "rel": "prev", - "url": build_absolute_uri(prev_url), + "url": request.build_absolute_uri(prev_url), }) if page_obj.has_next(): @@ -252,7 +251,7 @@ def _build_pagination_info( next_url = f"{base_url}&page={page_obj.next_page_number()}" pagination_links.append({ "rel": "next", - "url": build_absolute_uri(next_url), + "url": request.build_absolute_uri(next_url), }) return pagination_links or None @@ -321,7 +320,7 @@ def org_list_view(request: HttpRequest) -> HttpResponse: "@type": "CollectionPage", "name": "Twitch Organizations", "description": "List of Twitch organizations.", - "url": build_absolute_uri("/organizations/"), + "url": request.build_absolute_uri("/organizations/"), } seo_context: dict[str, Any] = _build_seo_context( @@ -364,7 +363,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 = build_absolute_uri( + url: str = request.build_absolute_uri( reverse("twitch:organization_detail", args=[organization.twitch_id]), ) organization_node: dict[str, Any] = { @@ -389,11 +388,11 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon # Breadcrumb schema breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([ - {"name": "Home", "url": build_absolute_uri("/")}, - {"name": "Organizations", "url": build_absolute_uri("/organizations/")}, + {"name": "Home", "url": request.build_absolute_uri("/")}, + {"name": "Organizations", "url": request.build_absolute_uri("/organizations/")}, { "name": org_name, - "url": build_absolute_uri( + "url": request.build_absolute_uri( reverse("twitch:organization_detail", args=[organization.twitch_id]), ), }, @@ -497,14 +496,14 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0 "@type": "CollectionPage", "name": title, "description": description, - "url": build_absolute_uri(base_url), + "url": request.build_absolute_uri(base_url), } seo_context: dict[str, Any] = _build_seo_context( page_title=title, page_description=description, seo_meta={ - "page_url": build_absolute_uri(base_url), + "page_url": request.build_absolute_uri(base_url), "pagination_info": pagination_info, "schema_data": collection_schema, }, @@ -637,7 +636,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo campaign.image_height if campaign.image_file else None ) - url: str = build_absolute_uri( + url: str = request.build_absolute_uri( reverse("twitch:campaign_detail", args=[campaign.twitch_id]), ) @@ -668,7 +667,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo else "Twitch" ) campaign_owner_url: str = ( - build_absolute_uri( + request.build_absolute_uri( reverse("twitch:organization_detail", args=[campaign_owner.twitch_id]), ) if campaign_owner @@ -702,17 +701,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": build_absolute_uri("/")}, - {"name": "Games", "url": build_absolute_uri("/games/")}, + {"name": "Home", "url": request.build_absolute_uri("/")}, + {"name": "Games", "url": request.build_absolute_uri("/games/")}, { "name": game_name, - "url": build_absolute_uri( + "url": request.build_absolute_uri( reverse("twitch:game_detail", args=[campaign.game.twitch_id]), ), }, { "name": campaign_name, - "url": build_absolute_uri( + "url": request.build_absolute_uri( reverse("twitch:campaign_detail", args=[campaign.twitch_id]), ), }, @@ -822,7 +821,7 @@ class GamesGridView(ListView): "@type": "CollectionPage", "name": "Twitch Games", "description": "Twitch games that had or have Twitch drops.", - "url": build_absolute_uri("/games/"), + "url": self.request.build_absolute_uri("/games/"), } seo_context: dict[str, Any] = _build_seo_context( @@ -978,7 +977,7 @@ class GameDetailView(DetailView): "@type": "VideoGame", "name": game_name, "description": game_description, - "url": build_absolute_uri( + "url": self.request.build_absolute_uri( reverse("twitch:game_detail", args=[game.twitch_id]), ), } @@ -993,7 +992,7 @@ class GameDetailView(DetailView): else "Twitch" ) owner_url: str = ( - build_absolute_uri( + self.request.build_absolute_uri( reverse("twitch:organization_detail", args=[preferred_owner.twitch_id]), ) if preferred_owner @@ -1015,11 +1014,11 @@ class GameDetailView(DetailView): # Breadcrumb schema breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([ - {"name": "Home", "url": build_absolute_uri("/")}, - {"name": "Games", "url": build_absolute_uri("/games/")}, + {"name": "Home", "url": self.request.build_absolute_uri("/")}, + {"name": "Games", "url": self.request.build_absolute_uri("/games/")}, { "name": game_name, - "url": build_absolute_uri( + "url": self.request.build_absolute_uri( reverse("twitch:game_detail", args=[game.twitch_id]), ), }, @@ -1115,12 +1114,12 @@ def dashboard(request: HttpRequest) -> HttpResponse: "@context": "https://schema.org", "@type": "WebSite", "name": "ttvdrops", - "url": build_absolute_uri("/"), + "url": request.build_absolute_uri("/"), "potentialAction": { "@type": "SearchAction", "target": { "@type": "EntryPoint", - "urlTemplate": build_absolute_uri( + "urlTemplate": request.build_absolute_uri( "/search/?q={search_term_string}", ), }, @@ -1220,14 +1219,14 @@ def reward_campaign_list_view(request: HttpRequest) -> HttpResponse: "@type": "CollectionPage", "name": title, "description": description, - "url": build_absolute_uri(base_url), + "url": request.build_absolute_uri(base_url), } seo_context: dict[str, Any] = _build_seo_context( page_title=title, page_description=description, seo_meta={ - "page_url": build_absolute_uri(base_url), + "page_url": request.build_absolute_uri(base_url), "pagination_info": pagination_info, "schema_data": collection_schema, }, @@ -1276,7 +1275,7 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes else f"{campaign_name}" ) - reward_url: str = build_absolute_uri( + reward_url: str = request.build_absolute_uri( reverse("twitch:reward_campaign_detail", args=[reward_campaign.twitch_id]), ) @@ -1317,14 +1316,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": build_absolute_uri("/")}, + {"name": "Home", "url": request.build_absolute_uri("/")}, { "name": "Reward Campaigns", - "url": build_absolute_uri("/reward-campaigns/"), + "url": request.build_absolute_uri("/reward-campaigns/"), }, { "name": campaign_name, - "url": build_absolute_uri( + "url": request.build_absolute_uri( reverse( "twitch:reward_campaign_detail", args=[reward_campaign.twitch_id], @@ -1419,14 +1418,14 @@ class ChannelListView(ListView): "@type": "CollectionPage", "name": "Twitch Channels", "description": "List of Twitch channels participating in drop campaigns.", - "url": build_absolute_uri("/channels/"), + "url": self.request.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": build_absolute_uri(base_url), + "page_url": self.request.build_absolute_uri(base_url), "pagination_info": pagination_info, "schema_data": collection_schema, }, @@ -1543,7 +1542,7 @@ class ChannelDetailView(DetailView): if total_campaigns > 1: description += "s" - channel_url: str = build_absolute_uri( + channel_url: str = self.request.build_absolute_uri( reverse("twitch:channel_detail", args=[channel.twitch_id]), ) channel_node: dict[str, Any] = { @@ -1570,11 +1569,11 @@ class ChannelDetailView(DetailView): # Breadcrumb schema breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([ - {"name": "Home", "url": build_absolute_uri("/")}, - {"name": "Channels", "url": build_absolute_uri("/channels/")}, + {"name": "Home", "url": self.request.build_absolute_uri("/")}, + {"name": "Channels", "url": self.request.build_absolute_uri("/channels/")}, { "name": name, - "url": build_absolute_uri( + "url": self.request.build_absolute_uri( reverse("twitch:channel_detail", args=[channel.twitch_id]), ), }, @@ -1639,7 +1638,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": build_absolute_uri("/badges/"), + "url": request.build_absolute_uri("/badges/"), } seo_context: dict[str, Any] = _build_seo_context( @@ -1723,7 +1722,7 @@ def badge_set_detail_view(request: HttpRequest, set_id: str) -> HttpResponse: "@type": "ItemList", "name": badge_set_name, "description": badge_set_description, - "url": build_absolute_uri( + "url": request.build_absolute_uri( reverse("twitch:badge_set_detail", args=[badge_set.set_id]), ), } diff --git a/youtube/views.py b/youtube/views.py index dd6aa18..5695dba 100644 --- a/youtube/views.py +++ b/youtube/views.py @@ -5,8 +5,6 @@ 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 @@ -138,7 +136,7 @@ def index(request: HttpRequest) -> HttpResponse: "@type": "ItemList", "name": PAGE_TITLE, "description": PAGE_DESCRIPTION, - "url": build_absolute_uri(request=request), + "url": request.build_absolute_uri(), "itemListElement": list_items, }