Compare commits

..

No commits in common. "d4fd35769dae891c3f2548478d3b0ab6dfa1cb02" and "e74472040edd5da0b726fe88bf94aad560ee7bb5" have entirely different histories.

13 changed files with 175 additions and 257 deletions

View file

@ -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"]

View file

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

View file

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

View file

@ -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'<link\s+rel="canonical"\s+href="https://ttvdrops\.lovinator\.space/drops/"\s*/?>',
assert (
'<link rel="canonical" href="https://ttvdrops.lovinator.space/drops/" />'
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'<link\s+rel="canonical"\s+href="https://ttvdrops\.lovinator\.space/custom-page/"\s*/?>',
assert (
'<link rel="canonical" href="https://ttvdrops.lovinator.space/custom-page/" />'
in content
)
assert canonical_pattern.search(content) is not None
def test_meta_tags_twitter_card_is_summary_without_image() -> None:

View file

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

View file

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

View file

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

View file

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

View file

@ -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] = {

View file

@ -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="{% if page_url %}{{ page_url }}{% else %}{{ BASE_URL }}{{ request.get_full_path }}{% endif %}" />
content="{% firstof page_url request.build_absolute_uri %}" />
{% if page_image %}
<meta property="og:image" content="{{ page_image }}" />
{% if page_image_width and page_image_height %}
@ -41,8 +41,7 @@
{% 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.' %}" />
@ -51,13 +50,20 @@
{% 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="{% if page_url %}{{ page_url }}{% else %}{{ BASE_URL }}{{ request.get_full_path }}{% endif %}" />
<link rel="canonical" href="{% firstof page_url request.build_absolute_uri %}" />
{# 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 %}

View file

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

View file

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

View file

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