Compare commits
No commits in common. "d4fd35769dae891c3f2548478d3b0ab6dfa1cb02" and "e74472040edd5da0b726fe88bf94aad560ee7bb5" have entirely different histories.
d4fd35769d
...
e74472040e
13 changed files with 175 additions and 257 deletions
|
|
@ -27,7 +27,7 @@ repos:
|
||||||
args: [--target-version, "6.0"]
|
args: [--target-version, "6.0"]
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.15.9
|
rev: v0.15.8
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args: ["--fix", "--exit-non-zero-on-fix"]
|
args: ["--fix", "--exit-non-zero-on-fix"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from chzzk.schemas import ChzzkCampaignV2
|
||||||
|
from chzzk.schemas import ChzzkRewardV2
|
||||||
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
|
|
@ -10,12 +17,6 @@ from chzzk.models import ChzzkCampaign
|
||||||
from chzzk.models import ChzzkReward
|
from chzzk.models import ChzzkReward
|
||||||
from chzzk.schemas import ChzzkApiResponseV2
|
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_THRESHOLD: int = 100_000_000
|
||||||
MAX_CAMPAIGN_OUTLIER_GAP: int = 1_000
|
MAX_CAMPAIGN_OUTLIER_GAP: int = 1_000
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,6 @@ TEMPLATES: list[dict[str, Any]] = [
|
||||||
"context_processors": [
|
"context_processors": [
|
||||||
"django.template.context_processors.debug",
|
"django.template.context_processors.debug",
|
||||||
"django.template.context_processors.request",
|
"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"
|
BASE_URL: str = "https://ttvdrops.lovinator.space"
|
||||||
# Allow overriding BASE_URL in tests via environment when needed
|
# Allow overriding BASE_URL in tests via environment when needed
|
||||||
BASE_URL = os.getenv("BASE_URL", BASE_URL)
|
BASE_URL = os.getenv("BASE_URL", BASE_URL)
|
||||||
|
|
||||||
|
# Monkeypatch HttpRequest.build_absolute_uri to prefer BASE_URL for absolute URLs
|
||||||
|
try:
|
||||||
|
from django.http.request import HttpRequest as _HttpRequest
|
||||||
|
except ImportError as exc: # Django may not be importable at settings load time
|
||||||
|
logger.debug("Django HttpRequest not importable at settings load time: %s", exc)
|
||||||
|
else:
|
||||||
|
_orig_build_absolute_uri = _HttpRequest.build_absolute_uri
|
||||||
|
|
||||||
|
def _ttvdrops_build_absolute_uri(
|
||||||
|
self: _HttpRequest,
|
||||||
|
location: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Prefer settings.BASE_URL when building absolute URIs for relative paths.
|
||||||
|
|
||||||
|
This makes test output deterministic (uses https://ttvdrops.lovinator.space)
|
||||||
|
instead of Django's test client default of http://testserver.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: An absolute URL constructed from BASE_URL and the provided location.
|
||||||
|
"""
|
||||||
|
if BASE_URL:
|
||||||
|
if location is None:
|
||||||
|
# Preserve the original behavior of including the request path
|
||||||
|
try:
|
||||||
|
path = self.get_full_path()
|
||||||
|
return BASE_URL.rstrip("/") + path
|
||||||
|
except AttributeError as exc:
|
||||||
|
logger.debug(
|
||||||
|
"Failed to get request path for build_absolute_uri: %s",
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
return BASE_URL if BASE_URL.endswith("/") else f"{BASE_URL}/"
|
||||||
|
if isinstance(location, str) and location.startswith("/"):
|
||||||
|
return BASE_URL.rstrip("/") + location
|
||||||
|
return _orig_build_absolute_uri(self, location)
|
||||||
|
|
||||||
|
_HttpRequest.build_absolute_uri = _ttvdrops_build_absolute_uri
|
||||||
|
|
||||||
|
# Ensure request.is_secure reports True so syndication.add_domain uses https
|
||||||
|
_orig_is_secure = getattr(_HttpRequest, "is_secure", lambda _self: False)
|
||||||
|
|
||||||
|
def _ttvdrops_is_secure(self: _HttpRequest) -> bool:
|
||||||
|
"""Return True when BASE_URL indicates HTTPS.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True when BASE_URL starts with 'https://', else defers to the
|
||||||
|
original is_secure implementation.
|
||||||
|
"""
|
||||||
|
return BASE_URL.startswith("https://")
|
||||||
|
|
||||||
|
_HttpRequest.is_secure = _ttvdrops_is_secure
|
||||||
|
|
||||||
|
# Monkeypatch django.contrib.sites.shortcuts.get_current_site to prefer BASE_URL
|
||||||
|
try:
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
from django.contrib.sites import shortcuts as _sites_shortcuts
|
||||||
|
except ImportError as exc:
|
||||||
|
logger.debug("Django sites.shortcuts not importable at settings load time: %s", exc)
|
||||||
|
else:
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _TTVDropsSite:
|
||||||
|
domain: str
|
||||||
|
|
||||||
|
def _ttvdrops_get_current_site(request: object) -> _TTVDropsSite:
|
||||||
|
"""Return a simple site-like object using the configured BASE_URL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Ignored; present for signature compatibility with
|
||||||
|
django.contrib.sites.shortcuts.get_current_site.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_TTVDropsSite: Object exposing a `domain` attribute derived from
|
||||||
|
settings.BASE_URL.
|
||||||
|
"""
|
||||||
|
parts = urlsplit(BASE_URL)
|
||||||
|
domain = parts.netloc or parts.path
|
||||||
|
return _TTVDropsSite(domain=domain)
|
||||||
|
|
||||||
|
_sites_shortcuts.get_current_site = _ttvdrops_get_current_site
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,10 @@ def test_meta_tags_use_request_absolute_url_for_og_url_and_canonical() -> None:
|
||||||
_extract_meta_content(content, "og:url")
|
_extract_meta_content(content, "og:url")
|
||||||
== "https://ttvdrops.lovinator.space/drops/"
|
== "https://ttvdrops.lovinator.space/drops/"
|
||||||
)
|
)
|
||||||
canonical_pattern: re.Pattern[str] = re.compile(
|
assert (
|
||||||
r'<link\s+rel="canonical"\s+href="https://ttvdrops\.lovinator\.space/drops/"\s*/?>',
|
'<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:
|
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")
|
_extract_meta_content(content, "og:url")
|
||||||
== "https://ttvdrops.lovinator.space/custom-page/"
|
== "https://ttvdrops.lovinator.space/custom-page/"
|
||||||
)
|
)
|
||||||
canonical_pattern: re.Pattern[str] = re.compile(
|
assert (
|
||||||
r'<link\s+rel="canonical"\s+href="https://ttvdrops\.lovinator\.space/custom-page/"\s*/?>',
|
'<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:
|
def test_meta_tags_twitter_card_is_summary_without_image() -> None:
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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", ""),
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -26,7 +26,6 @@ from django.template.defaultfilters import filesizeformat
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from core.base_url import build_absolute_uri
|
|
||||||
from kick.models import KickChannel
|
from kick.models import KickChannel
|
||||||
from kick.models import KickDropCampaign
|
from kick.models import KickDropCampaign
|
||||||
from twitch.models import Channel
|
from twitch.models import Channel
|
||||||
|
|
@ -508,7 +507,7 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Feed Documentation",
|
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_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(
|
return render(
|
||||||
|
|
@ -722,7 +721,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
|
||||||
dataset_distributions.append({
|
dataset_distributions.append({
|
||||||
"@type": "DataDownload",
|
"@type": "DataDownload",
|
||||||
"name": dataset["name"],
|
"name": dataset["name"],
|
||||||
"contentUrl": build_absolute_uri(
|
"contentUrl": request.build_absolute_uri(
|
||||||
reverse("core:dataset_backup_download", args=[download_path]),
|
reverse("core:dataset_backup_download", args=[download_path]),
|
||||||
),
|
),
|
||||||
"encodingFormat": "application/zstd",
|
"encodingFormat": "application/zstd",
|
||||||
|
|
@ -732,9 +731,9 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Dataset",
|
"@type": "Dataset",
|
||||||
"name": "Historical archive of Twitch and Kick drop data",
|
"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/..",
|
"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/",
|
"license": "https://creativecommons.org/publicdomain/zero/1.0/",
|
||||||
"isAccessibleForFree": True,
|
"isAccessibleForFree": True,
|
||||||
"description": (
|
"description": (
|
||||||
|
|
@ -754,7 +753,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
|
||||||
"includedInDataCatalog": {
|
"includedInDataCatalog": {
|
||||||
"@type": "DataCatalog",
|
"@type": "DataCatalog",
|
||||||
"name": "ttvdrops.lovinator.space",
|
"name": "ttvdrops.lovinator.space",
|
||||||
"url": build_absolute_uri(reverse("core:dataset_backups")),
|
"url": request.build_absolute_uri(reverse("core:dataset_backups")),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if dataset_distributions:
|
if dataset_distributions:
|
||||||
|
|
@ -906,7 +905,7 @@ def search_view(request: HttpRequest) -> HttpResponse:
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=page_title,
|
page_title=page_title,
|
||||||
page_description=page_description,
|
page_description=page_description,
|
||||||
page_url=build_absolute_uri(reverse("core:search")),
|
page_url=request.build_absolute_uri(reverse("core:search")),
|
||||||
)
|
)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|
@ -1007,12 +1006,12 @@ def dashboard(request: HttpRequest) -> HttpResponse:
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": "ttvdrops",
|
"name": "ttvdrops",
|
||||||
"url": build_absolute_uri("/"),
|
"url": request.build_absolute_uri("/"),
|
||||||
"potentialAction": {
|
"potentialAction": {
|
||||||
"@type": "SearchAction",
|
"@type": "SearchAction",
|
||||||
"target": {
|
"target": {
|
||||||
"@type": "EntryPoint",
|
"@type": "EntryPoint",
|
||||||
"urlTemplate": build_absolute_uri(
|
"urlTemplate": request.build_absolute_uri(
|
||||||
"/search/?q={search_term_string}",
|
"/search/?q={search_term_string}",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ from django.shortcuts import render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from core.base_url import build_absolute_uri
|
|
||||||
from kick.models import KickCategory
|
from kick.models import KickCategory
|
||||||
from kick.models import KickDropCampaign
|
from kick.models import KickDropCampaign
|
||||||
from kick.models import KickOrganization
|
from kick.models import KickOrganization
|
||||||
|
|
@ -128,14 +127,14 @@ def _build_pagination_info(
|
||||||
if page_obj.has_previous():
|
if page_obj.has_previous():
|
||||||
links.append({
|
links.append({
|
||||||
"rel": "prev",
|
"rel": "prev",
|
||||||
"url": build_absolute_uri(
|
"url": request.build_absolute_uri(
|
||||||
f"{base_url}{sep}page={page_obj.previous_page_number()}",
|
f"{base_url}{sep}page={page_obj.previous_page_number()}",
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
if page_obj.has_next():
|
if page_obj.has_next():
|
||||||
links.append({
|
links.append({
|
||||||
"rel": "next",
|
"rel": "next",
|
||||||
"url": build_absolute_uri(
|
"url": request.build_absolute_uri(
|
||||||
f"{base_url}{sep}page={page_obj.next_page_number()}",
|
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(
|
seo_context: dict[str, str] = _build_seo_context(
|
||||||
page_title=title,
|
page_title=title,
|
||||||
page_description="Browse Kick drop campaigns.",
|
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(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|
@ -292,20 +291,20 @@ def campaign_detail_view(request: HttpRequest, kick_id: str) -> HttpResponse:
|
||||||
channels: list[KickChannel] = list(campaign.channels.select_related("user"))
|
channels: list[KickChannel] = list(campaign.channels.select_related("user"))
|
||||||
|
|
||||||
breadcrumb_schema: str = _build_breadcrumb_schema([
|
breadcrumb_schema: str = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": build_absolute_uri("/")},
|
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||||
{
|
{
|
||||||
"name": "Kick Campaigns",
|
"name": "Kick Campaigns",
|
||||||
"url": build_absolute_uri(reverse("kick:campaign_list")),
|
"url": request.build_absolute_uri(reverse("kick:campaign_list")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": campaign.name,
|
"name": campaign.name,
|
||||||
"url": build_absolute_uri(
|
"url": request.build_absolute_uri(
|
||||||
reverse("kick:campaign_detail", args=[campaign.kick_id]),
|
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]),
|
reverse("kick:campaign_detail", args=[campaign.kick_id]),
|
||||||
)
|
)
|
||||||
campaign_event: dict[str, Any] = {
|
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([
|
breadcrumb_schema: str = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": build_absolute_uri("/")},
|
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||||
{
|
{
|
||||||
"name": "Kick Games",
|
"name": "Kick Games",
|
||||||
"url": build_absolute_uri(reverse("kick:game_list")),
|
"url": request.build_absolute_uri(reverse("kick:game_list")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": category.name,
|
"name": category.name,
|
||||||
"url": build_absolute_uri(
|
"url": request.build_absolute_uri(
|
||||||
reverse("kick:game_detail", args=[category.kick_id]),
|
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]),
|
reverse("kick:game_detail", args=[category.kick_id]),
|
||||||
)
|
)
|
||||||
category_schema: dict[str, Any] = {
|
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([
|
breadcrumb_schema: str = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": build_absolute_uri("/")},
|
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||||
{
|
{
|
||||||
"name": "Kick Organizations",
|
"name": "Kick Organizations",
|
||||||
"url": build_absolute_uri(reverse("kick:organization_list")),
|
"url": request.build_absolute_uri(reverse("kick:organization_list")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": org.name,
|
"name": org.name,
|
||||||
"url": build_absolute_uri(
|
"url": request.build_absolute_uri(
|
||||||
reverse("kick:organization_detail", args=[org.kick_id]),
|
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]),
|
reverse("kick:organization_detail", args=[org.kick_id]),
|
||||||
)
|
)
|
||||||
organization_node: dict[str, Any] = {
|
organization_node: dict[str, Any] = {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
content="{% firstof page_description 'ttvdrops - Track Twitch drops.' %}" />
|
content="{% firstof page_description 'ttvdrops - Track Twitch drops.' %}" />
|
||||||
<meta property="og:type" content="{% firstof og_type 'website' %}" />
|
<meta property="og:type" content="{% firstof og_type 'website' %}" />
|
||||||
<meta property="og:url"
|
<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 %}
|
{% if page_image %}
|
||||||
<meta property="og:image" content="{{ page_image }}" />
|
<meta property="og:image" content="{{ page_image }}" />
|
||||||
{% if page_image_width and page_image_height %}
|
{% if page_image_width and page_image_height %}
|
||||||
|
|
@ -41,8 +41,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Twitter Card tags for rich previews #}
|
{# Twitter Card tags for rich previews #}
|
||||||
<meta name="twitter:card"
|
<meta name="twitter:card" content="{% if page_image %}summary_large_image{% else %}summary{% endif %}" />
|
||||||
content="{% if page_image %}summary_large_image{% else %}summary{% endif %}" />
|
|
||||||
<meta name="twitter:title" content="{% firstof page_title 'ttvdrops' %}" />
|
<meta name="twitter:title" content="{% firstof page_title 'ttvdrops' %}" />
|
||||||
<meta name="twitter:description"
|
<meta name="twitter:description"
|
||||||
content="{% firstof page_description 'ttvdrops - Twitch and Kick drops.' %}" />
|
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 published_date %}<meta property="article:published_time" content="{{ published_date }}" />{% endif %}
|
||||||
{% if modified_date %}<meta property="article:modified_time" content="{{ modified_date }}" />{% endif %}
|
{% if modified_date %}<meta property="article:modified_time" content="{{ modified_date }}" />{% endif %}
|
||||||
{# Canonical tag #}
|
{# Canonical tag #}
|
||||||
<link rel="canonical"
|
<link rel="canonical" href="{% firstof page_url request.build_absolute_uri %}" />
|
||||||
href="{% if page_url %}{{ page_url }}{% else %}{{ BASE_URL }}{{ request.get_full_path }}{% endif %}" />
|
|
||||||
{# Pagination links (for crawler efficiency) #}
|
{# Pagination links (for crawler efficiency) #}
|
||||||
{% if pagination_info %}
|
{% if pagination_info %}
|
||||||
{% for link in pagination_info %}<link rel="{{ link.rel }}" href="{{ link.url }}" />{% endfor %}
|
{% for link in pagination_info %}<link rel="{{ link.rel }}" href="{{ link.url }}" />{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Schema.org JSON-LD structured data #}
|
{# 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 #}
|
{# Breadcrumb schema #}
|
||||||
{% if breadcrumb_schema %}<script type="application/ld+json">{{ breadcrumb_schema|safe }}</script>{% endif %}
|
{% if breadcrumb_schema %}
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{{ breadcrumb_schema|safe }}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from collections.abc import Callable
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Literal
|
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.humanize.templatetags.humanize import naturaltime
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
|
||||||
from django.contrib.syndication.views import Feed
|
from django.contrib.syndication.views import Feed
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.db.models.query import QuerySet
|
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.html import format_html_join
|
||||||
from django.utils.safestring import SafeText
|
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 Channel
|
||||||
from twitch.models import ChatBadge
|
from twitch.models import ChatBadge
|
||||||
from twitch.models import DropCampaign
|
from twitch.models import DropCampaign
|
||||||
|
|
@ -32,15 +26,11 @@ from twitch.models import TimeBasedDrop
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import datetime
|
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 Model
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.utils.feedgenerator import SyndicationFeed
|
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
|
|
||||||
from twitch.models import DropBenefit
|
from twitch.models import DropBenefit
|
||||||
|
|
@ -109,14 +99,14 @@ class TTVDropsBaseFeed(Feed):
|
||||||
"""
|
"""
|
||||||
if self._request is None:
|
if self._request is None:
|
||||||
return url
|
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]:
|
def _absolute_stylesheet_urls(self, request: HttpRequest) -> list[str]:
|
||||||
"""Return stylesheet URLs as absolute HTTP URLs for browser/file compatibility."""
|
"""Return stylesheet URLs as absolute HTTP URLs for browser/file compatibility."""
|
||||||
return [
|
return [
|
||||||
href
|
href
|
||||||
if href.startswith(("http://", "https://"))
|
if href.startswith(("http://", "https://"))
|
||||||
else build_absolute_uri(href, request=request)
|
else request.build_absolute_uri(href)
|
||||||
for href in self.stylesheets
|
for href in self.stylesheets
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -161,41 +151,6 @@ class TTVDropsBaseFeed(Feed):
|
||||||
|
|
||||||
response.content = content.encode(encoding)
|
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__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ from django.utils import timezone
|
||||||
from django.views.generic import DetailView
|
from django.views.generic import DetailView
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
||||||
from core.base_url import build_absolute_uri
|
|
||||||
from twitch.models import Channel
|
from twitch.models import Channel
|
||||||
from twitch.models import ChatBadge
|
from twitch.models import ChatBadge
|
||||||
from twitch.models import ChatBadgeSet
|
from twitch.models import ChatBadgeSet
|
||||||
|
|
@ -100,7 +99,7 @@ def _build_image_object(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"@type": "ImageObject",
|
"@type": "ImageObject",
|
||||||
"contentUrl": build_absolute_uri(image_url),
|
"contentUrl": request.build_absolute_uri(image_url),
|
||||||
"creditText": creator_name,
|
"creditText": creator_name,
|
||||||
"copyrightNotice": copyright_notice or creator_name,
|
"copyrightNotice": copyright_notice or creator_name,
|
||||||
"creator": creator,
|
"creator": creator,
|
||||||
|
|
@ -242,7 +241,7 @@ def _build_pagination_info(
|
||||||
prev_url = f"{base_url}&page={page_obj.previous_page_number()}"
|
prev_url = f"{base_url}&page={page_obj.previous_page_number()}"
|
||||||
pagination_links.append({
|
pagination_links.append({
|
||||||
"rel": "prev",
|
"rel": "prev",
|
||||||
"url": build_absolute_uri(prev_url),
|
"url": request.build_absolute_uri(prev_url),
|
||||||
})
|
})
|
||||||
|
|
||||||
if page_obj.has_next():
|
if page_obj.has_next():
|
||||||
|
|
@ -252,7 +251,7 @@ def _build_pagination_info(
|
||||||
next_url = f"{base_url}&page={page_obj.next_page_number()}"
|
next_url = f"{base_url}&page={page_obj.next_page_number()}"
|
||||||
pagination_links.append({
|
pagination_links.append({
|
||||||
"rel": "next",
|
"rel": "next",
|
||||||
"url": build_absolute_uri(next_url),
|
"url": request.build_absolute_uri(next_url),
|
||||||
})
|
})
|
||||||
|
|
||||||
return pagination_links or None
|
return pagination_links or None
|
||||||
|
|
@ -321,7 +320,7 @@ def org_list_view(request: HttpRequest) -> HttpResponse:
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "Twitch Organizations",
|
"name": "Twitch Organizations",
|
||||||
"description": "List of 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(
|
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"
|
s: Literal["", "s"] = "" if games_count == 1 else "s"
|
||||||
org_description: str = f"{org_name} has {games_count} game{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]),
|
reverse("twitch:organization_detail", args=[organization.twitch_id]),
|
||||||
)
|
)
|
||||||
organization_node: dict[str, Any] = {
|
organization_node: dict[str, Any] = {
|
||||||
|
|
@ -389,11 +388,11 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon
|
||||||
|
|
||||||
# Breadcrumb schema
|
# Breadcrumb schema
|
||||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": build_absolute_uri("/")},
|
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||||
{"name": "Organizations", "url": build_absolute_uri("/organizations/")},
|
{"name": "Organizations", "url": request.build_absolute_uri("/organizations/")},
|
||||||
{
|
{
|
||||||
"name": org_name,
|
"name": org_name,
|
||||||
"url": build_absolute_uri(
|
"url": request.build_absolute_uri(
|
||||||
reverse("twitch:organization_detail", args=[organization.twitch_id]),
|
reverse("twitch:organization_detail", args=[organization.twitch_id]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -497,14 +496,14 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": title,
|
"name": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": build_absolute_uri(base_url),
|
"url": request.build_absolute_uri(base_url),
|
||||||
}
|
}
|
||||||
|
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=title,
|
page_title=title,
|
||||||
page_description=description,
|
page_description=description,
|
||||||
seo_meta={
|
seo_meta={
|
||||||
"page_url": build_absolute_uri(base_url),
|
"page_url": request.build_absolute_uri(base_url),
|
||||||
"pagination_info": pagination_info,
|
"pagination_info": pagination_info,
|
||||||
"schema_data": collection_schema,
|
"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
|
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]),
|
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"
|
else "Twitch"
|
||||||
)
|
)
|
||||||
campaign_owner_url: str = (
|
campaign_owner_url: str = (
|
||||||
build_absolute_uri(
|
request.build_absolute_uri(
|
||||||
reverse("twitch:organization_detail", args=[campaign_owner.twitch_id]),
|
reverse("twitch:organization_detail", args=[campaign_owner.twitch_id]),
|
||||||
)
|
)
|
||||||
if campaign_owner
|
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
|
campaign.game.display_name or campaign.game.name or campaign.game.twitch_id
|
||||||
)
|
)
|
||||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": build_absolute_uri("/")},
|
{"name": "Home", "url": request.build_absolute_uri("/")},
|
||||||
{"name": "Games", "url": build_absolute_uri("/games/")},
|
{"name": "Games", "url": request.build_absolute_uri("/games/")},
|
||||||
{
|
{
|
||||||
"name": game_name,
|
"name": game_name,
|
||||||
"url": build_absolute_uri(
|
"url": request.build_absolute_uri(
|
||||||
reverse("twitch:game_detail", args=[campaign.game.twitch_id]),
|
reverse("twitch:game_detail", args=[campaign.game.twitch_id]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": campaign_name,
|
"name": campaign_name,
|
||||||
"url": build_absolute_uri(
|
"url": request.build_absolute_uri(
|
||||||
reverse("twitch:campaign_detail", args=[campaign.twitch_id]),
|
reverse("twitch:campaign_detail", args=[campaign.twitch_id]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -822,7 +821,7 @@ class GamesGridView(ListView):
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "Twitch Games",
|
"name": "Twitch Games",
|
||||||
"description": "Twitch games that had or have Twitch drops.",
|
"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(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
|
|
@ -978,7 +977,7 @@ class GameDetailView(DetailView):
|
||||||
"@type": "VideoGame",
|
"@type": "VideoGame",
|
||||||
"name": game_name,
|
"name": game_name,
|
||||||
"description": game_description,
|
"description": game_description,
|
||||||
"url": build_absolute_uri(
|
"url": self.request.build_absolute_uri(
|
||||||
reverse("twitch:game_detail", args=[game.twitch_id]),
|
reverse("twitch:game_detail", args=[game.twitch_id]),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
@ -993,7 +992,7 @@ class GameDetailView(DetailView):
|
||||||
else "Twitch"
|
else "Twitch"
|
||||||
)
|
)
|
||||||
owner_url: str = (
|
owner_url: str = (
|
||||||
build_absolute_uri(
|
self.request.build_absolute_uri(
|
||||||
reverse("twitch:organization_detail", args=[preferred_owner.twitch_id]),
|
reverse("twitch:organization_detail", args=[preferred_owner.twitch_id]),
|
||||||
)
|
)
|
||||||
if preferred_owner
|
if preferred_owner
|
||||||
|
|
@ -1015,11 +1014,11 @@ class GameDetailView(DetailView):
|
||||||
|
|
||||||
# Breadcrumb schema
|
# Breadcrumb schema
|
||||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": build_absolute_uri("/")},
|
{"name": "Home", "url": self.request.build_absolute_uri("/")},
|
||||||
{"name": "Games", "url": build_absolute_uri("/games/")},
|
{"name": "Games", "url": self.request.build_absolute_uri("/games/")},
|
||||||
{
|
{
|
||||||
"name": game_name,
|
"name": game_name,
|
||||||
"url": build_absolute_uri(
|
"url": self.request.build_absolute_uri(
|
||||||
reverse("twitch:game_detail", args=[game.twitch_id]),
|
reverse("twitch:game_detail", args=[game.twitch_id]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -1115,12 +1114,12 @@ def dashboard(request: HttpRequest) -> HttpResponse:
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": "ttvdrops",
|
"name": "ttvdrops",
|
||||||
"url": build_absolute_uri("/"),
|
"url": request.build_absolute_uri("/"),
|
||||||
"potentialAction": {
|
"potentialAction": {
|
||||||
"@type": "SearchAction",
|
"@type": "SearchAction",
|
||||||
"target": {
|
"target": {
|
||||||
"@type": "EntryPoint",
|
"@type": "EntryPoint",
|
||||||
"urlTemplate": build_absolute_uri(
|
"urlTemplate": request.build_absolute_uri(
|
||||||
"/search/?q={search_term_string}",
|
"/search/?q={search_term_string}",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -1220,14 +1219,14 @@ def reward_campaign_list_view(request: HttpRequest) -> HttpResponse:
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": title,
|
"name": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"url": build_absolute_uri(base_url),
|
"url": request.build_absolute_uri(base_url),
|
||||||
}
|
}
|
||||||
|
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title=title,
|
page_title=title,
|
||||||
page_description=description,
|
page_description=description,
|
||||||
seo_meta={
|
seo_meta={
|
||||||
"page_url": build_absolute_uri(base_url),
|
"page_url": request.build_absolute_uri(base_url),
|
||||||
"pagination_info": pagination_info,
|
"pagination_info": pagination_info,
|
||||||
"schema_data": collection_schema,
|
"schema_data": collection_schema,
|
||||||
},
|
},
|
||||||
|
|
@ -1276,7 +1275,7 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes
|
||||||
else f"{campaign_name}"
|
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]),
|
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
|
||||||
breadcrumb_schema: dict[str, Any] = _build_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",
|
"name": "Reward Campaigns",
|
||||||
"url": build_absolute_uri("/reward-campaigns/"),
|
"url": request.build_absolute_uri("/reward-campaigns/"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": campaign_name,
|
"name": campaign_name,
|
||||||
"url": build_absolute_uri(
|
"url": request.build_absolute_uri(
|
||||||
reverse(
|
reverse(
|
||||||
"twitch:reward_campaign_detail",
|
"twitch:reward_campaign_detail",
|
||||||
args=[reward_campaign.twitch_id],
|
args=[reward_campaign.twitch_id],
|
||||||
|
|
@ -1419,14 +1418,14 @@ class ChannelListView(ListView):
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "Twitch Channels",
|
"name": "Twitch Channels",
|
||||||
"description": "List of Twitch channels participating in drop campaigns.",
|
"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(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Twitch Channels",
|
page_title="Twitch Channels",
|
||||||
page_description="List of Twitch channels participating in drop campaigns.",
|
page_description="List of Twitch channels participating in drop campaigns.",
|
||||||
seo_meta={
|
seo_meta={
|
||||||
"page_url": build_absolute_uri(base_url),
|
"page_url": self.request.build_absolute_uri(base_url),
|
||||||
"pagination_info": pagination_info,
|
"pagination_info": pagination_info,
|
||||||
"schema_data": collection_schema,
|
"schema_data": collection_schema,
|
||||||
},
|
},
|
||||||
|
|
@ -1543,7 +1542,7 @@ class ChannelDetailView(DetailView):
|
||||||
if total_campaigns > 1:
|
if total_campaigns > 1:
|
||||||
description += "s"
|
description += "s"
|
||||||
|
|
||||||
channel_url: str = build_absolute_uri(
|
channel_url: str = self.request.build_absolute_uri(
|
||||||
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
||||||
)
|
)
|
||||||
channel_node: dict[str, Any] = {
|
channel_node: dict[str, Any] = {
|
||||||
|
|
@ -1570,11 +1569,11 @@ class ChannelDetailView(DetailView):
|
||||||
|
|
||||||
# Breadcrumb schema
|
# Breadcrumb schema
|
||||||
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
breadcrumb_schema: dict[str, Any] = _build_breadcrumb_schema([
|
||||||
{"name": "Home", "url": build_absolute_uri("/")},
|
{"name": "Home", "url": self.request.build_absolute_uri("/")},
|
||||||
{"name": "Channels", "url": build_absolute_uri("/channels/")},
|
{"name": "Channels", "url": self.request.build_absolute_uri("/channels/")},
|
||||||
{
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"url": build_absolute_uri(
|
"url": self.request.build_absolute_uri(
|
||||||
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
reverse("twitch:channel_detail", args=[channel.twitch_id]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -1639,7 +1638,7 @@ def badge_list_view(request: HttpRequest) -> HttpResponse:
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "Twitch chat badges",
|
"name": "Twitch chat badges",
|
||||||
"description": "List of Twitch chat badges awarded through drop campaigns.",
|
"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(
|
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",
|
"@type": "ItemList",
|
||||||
"name": badge_set_name,
|
"name": badge_set_name,
|
||||||
"description": badge_set_description,
|
"description": badge_set_description,
|
||||||
"url": build_absolute_uri(
|
"url": request.build_absolute_uri(
|
||||||
reverse("twitch:badge_set_detail", args=[badge_set.set_id]),
|
reverse("twitch:badge_set_detail", args=[badge_set.set_id]),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ from typing import Any
|
||||||
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from core.base_url import build_absolute_uri
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
@ -138,7 +136,7 @@ def index(request: HttpRequest) -> HttpResponse:
|
||||||
"@type": "ItemList",
|
"@type": "ItemList",
|
||||||
"name": PAGE_TITLE,
|
"name": PAGE_TITLE,
|
||||||
"description": PAGE_DESCRIPTION,
|
"description": PAGE_DESCRIPTION,
|
||||||
"url": build_absolute_uri(request=request),
|
"url": request.build_absolute_uri(),
|
||||||
"itemListElement": list_items,
|
"itemListElement": list_items,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue