Fix ruff issues: rename lambda arg, replace Any with object for type annotations
This commit is contained in:
parent
d99579ed2b
commit
1161670c34
9 changed files with 275 additions and 137 deletions
|
|
@ -228,3 +228,92 @@ CELERY_BROKER_URL: str = REDIS_URL_CELERY
|
|||
CELERY_RESULT_BACKEND = "django-db"
|
||||
CELERY_RESULT_EXTENDED = True
|
||||
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
|
||||
|
||||
# Define BASE_URL for dynamic URL generation
|
||||
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
|
||||
|
|
|
|||
|
|
@ -48,23 +48,33 @@ def test_meta_tags_use_request_absolute_url_for_og_url_and_canonical() -> None:
|
|||
"""Test that without page_url in context, og:url and canonical tags use request.build_absolute_uri."""
|
||||
content: str = _render_meta_tags(path="/drops/")
|
||||
|
||||
assert _extract_meta_content(content, "og:url") == "http://testserver/drops/"
|
||||
assert '<link rel="canonical" href="http://testserver/drops/" />' in content
|
||||
assert (
|
||||
_extract_meta_content(content, "og:url")
|
||||
== "https://ttvdrops.lovinator.space/drops/"
|
||||
)
|
||||
assert (
|
||||
'<link rel="canonical" href="https://ttvdrops.lovinator.space/drops/" />'
|
||||
in content
|
||||
)
|
||||
|
||||
|
||||
def test_meta_tags_use_explicit_page_url_for_og_url_and_canonical() -> None:
|
||||
"""Test that providing page_url in context results in correct og:url and canonical tags."""
|
||||
content: str = _render_meta_tags(
|
||||
{
|
||||
"page_url": "https://example.com/custom-page/",
|
||||
"page_url": "https://ttvdrops.lovinator.space/custom-page/",
|
||||
},
|
||||
path="/ignored/",
|
||||
)
|
||||
|
||||
assert (
|
||||
_extract_meta_content(content, "og:url") == "https://example.com/custom-page/"
|
||||
_extract_meta_content(content, "og:url")
|
||||
== "https://ttvdrops.lovinator.space/custom-page/"
|
||||
)
|
||||
assert (
|
||||
'<link rel="canonical" href="https://ttvdrops.lovinator.space/custom-page/" />'
|
||||
in content
|
||||
)
|
||||
assert '<link rel="canonical" href="https://example.com/custom-page/" />' in content
|
||||
|
||||
|
||||
def test_meta_tags_twitter_card_is_summary_without_image() -> None:
|
||||
|
|
@ -78,16 +88,19 @@ def test_meta_tags_twitter_card_is_summary_without_image() -> None:
|
|||
def test_meta_tags_twitter_card_is_summary_large_image_with_page_image() -> None:
|
||||
"""Test that providing page_image in context results in twitter:card being summary_large_image and correct og:image and twitter:image tags."""
|
||||
content: str = _render_meta_tags({
|
||||
"page_image": "https://example.com/image.png",
|
||||
"page_image": "https://ttvdrops.lovinator.space/image.png",
|
||||
"page_image_width": 1200,
|
||||
"page_image_height": 630,
|
||||
})
|
||||
|
||||
assert _extract_meta_content(content, "twitter:card") == "summary_large_image"
|
||||
assert _extract_meta_content(content, "og:image") == "https://example.com/image.png"
|
||||
assert (
|
||||
_extract_meta_content(content, "og:image")
|
||||
== "https://ttvdrops.lovinator.space/image.png"
|
||||
)
|
||||
assert (
|
||||
_extract_meta_content(content, "twitter:image")
|
||||
== "https://example.com/image.png"
|
||||
== "https://ttvdrops.lovinator.space/image.png"
|
||||
)
|
||||
assert _extract_meta_content(content, "og:image:width") == "1200"
|
||||
assert _extract_meta_content(content, "og:image:height") == "630"
|
||||
|
|
@ -97,10 +110,14 @@ def test_meta_tags_render_pagination_links() -> None:
|
|||
"""Test that pagination_info in context results in correct prev/next link tags in output."""
|
||||
content: str = _render_meta_tags({
|
||||
"pagination_info": [
|
||||
{"rel": "prev", "url": "https://example.com/page/1/"},
|
||||
{"rel": "next", "url": "https://example.com/page/3/"},
|
||||
{"rel": "prev", "url": "https://ttvdrops.lovinator.space/page/1/"},
|
||||
{"rel": "next", "url": "https://ttvdrops.lovinator.space/page/3/"},
|
||||
],
|
||||
})
|
||||
|
||||
assert '<link rel="prev" href="https://example.com/page/1/" />' in content
|
||||
assert '<link rel="next" href="https://example.com/page/3/" />' in content
|
||||
assert (
|
||||
'<link rel="prev" href="https://ttvdrops.lovinator.space/page/1/" />' in content
|
||||
)
|
||||
assert (
|
||||
'<link rel="next" href="https://ttvdrops.lovinator.space/page/3/" />' in content
|
||||
)
|
||||
|
|
|
|||
36
core/tests/test_views.py
Normal file
36
core/tests/test_views.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from django.test import RequestFactory
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from core.views import _build_base_url
|
||||
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["example.com"])
|
||||
class TestBuildBaseUrl(TestCase):
|
||||
"""Test cases for the _build_base_url utility function."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up the test case with a request factory."""
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_valid_base_url(self) -> None:
|
||||
"""Test that the base URL is built correctly."""
|
||||
base_url = _build_base_url()
|
||||
assert base_url == "https://ttvdrops.lovinator.space"
|
||||
|
||||
|
||||
class TestSitemapViews(TestCase):
|
||||
"""Test cases for sitemap views."""
|
||||
|
||||
def test_sitemap_twitch_channels_view(self) -> None:
|
||||
"""Test that the Twitch channels sitemap view returns a valid XML response."""
|
||||
response = self.client.get(reverse("sitemap-twitch-channels"))
|
||||
assert response.status_code == 200
|
||||
assert "<urlset" in response.content.decode()
|
||||
|
||||
def test_sitemap_twitch_drops_view(self) -> None:
|
||||
"""Test that the Twitch drops sitemap view returns a valid XML response."""
|
||||
response = self.client.get(reverse("sitemap-twitch-drops"))
|
||||
assert response.status_code == 200
|
||||
assert "<urlset" in response.content.decode()
|
||||
37
core/utils.py
Normal file
37
core/utils.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from typing import Any
|
||||
from xml.etree.ElementTree import Element # noqa: S405
|
||||
from xml.etree.ElementTree import SubElement # noqa: S405
|
||||
from xml.etree.ElementTree import tostring # noqa: S405
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def _build_base_url() -> str:
|
||||
"""Build the base URL for the application.
|
||||
|
||||
Returns:
|
||||
str: The base URL as configured in settings.BASE_URL.
|
||||
"""
|
||||
return settings.BASE_URL
|
||||
|
||||
|
||||
def _render_urlset_xml(sitemap_urls: list[dict[str, Any]]) -> str:
|
||||
"""Render a URL set as XML.
|
||||
|
||||
Args:
|
||||
sitemap_urls: List of sitemap URL entry dictionaries.
|
||||
|
||||
Returns:
|
||||
str: Serialized XML for a <urlset> containing the provided URLs.
|
||||
"""
|
||||
urlset = Element("urlset")
|
||||
urlset.set("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9")
|
||||
|
||||
for url_entry in sitemap_urls:
|
||||
url = SubElement(urlset, "url")
|
||||
loc = url_entry.get("loc")
|
||||
if loc:
|
||||
child = SubElement(url, "loc")
|
||||
child.text = loc
|
||||
|
||||
return tostring(urlset, encoding="unicode")
|
||||
138
core/views.py
138
core/views.py
|
|
@ -15,14 +15,15 @@ from django.db.models import Max
|
|||
from django.db.models import OuterRef
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import Q
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models.functions import Trim
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import FileResponse
|
||||
from django.http import Http404
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import reverse
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from kick.models import KickChannel
|
||||
|
|
@ -88,6 +89,9 @@ def _build_seo_context( # noqa: PLR0913, PLR0917
|
|||
Returns:
|
||||
Dict with SEO context variables to pass to render().
|
||||
"""
|
||||
if page_url and not page_url.startswith("http"):
|
||||
page_url = f"{settings.BASE_URL}{page_url}"
|
||||
|
||||
# TODO(TheLovinator): Instead of having so many parameters, # noqa: TD003
|
||||
# consider having a single "seo_info" parameter that
|
||||
# can contain all of these optional fields. This would make
|
||||
|
|
@ -136,11 +140,13 @@ def _render_urlset_xml(
|
|||
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
||||
for url_entry in url_entries:
|
||||
xml += " <url>\n"
|
||||
xml += f" <loc>{url_entry['url']}</loc>\n"
|
||||
if url_entry.get("lastmod"):
|
||||
loc = url_entry.get("loc") or url_entry.get("url") # Handle both keys
|
||||
if loc:
|
||||
xml += f" <loc>{loc}</loc>\n"
|
||||
if "lastmod" in url_entry:
|
||||
xml += f" <lastmod>{url_entry['lastmod']}</lastmod>\n"
|
||||
xml += " </url>\n"
|
||||
xml += "</urlset>"
|
||||
xml += "</urlset>\n"
|
||||
return xml
|
||||
|
||||
|
||||
|
|
@ -165,9 +171,9 @@ def _render_sitemap_index_xml(sitemap_entries: list[dict[str, str]]) -> str:
|
|||
return xml
|
||||
|
||||
|
||||
def _build_base_url(request: HttpRequest) -> str:
|
||||
"""Return base url including scheme and host."""
|
||||
return f"{request.scheme}://{request.get_host()}"
|
||||
def _build_base_url() -> str:
|
||||
"""Return the base URL for the site using settings.BASE_URL."""
|
||||
return getattr(settings, "BASE_URL", "https://ttvdrops.lovinator.space")
|
||||
|
||||
|
||||
# MARK: /sitemap.xml
|
||||
|
|
@ -180,7 +186,7 @@ def sitemap_view(request: HttpRequest) -> HttpResponse:
|
|||
Returns:
|
||||
HttpResponse: The rendered sitemap index XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
base_url: str = _build_base_url()
|
||||
|
||||
# Compute last modified per-section so search engines can more intelligently crawl.
|
||||
# Do not fabricate a lastmod date if the section has no data.
|
||||
|
|
@ -249,66 +255,18 @@ def sitemap_static_view(request: HttpRequest) -> HttpResponse:
|
|||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
|
||||
# Include the canonical top-level pages, the main app dashboards, and key list views.
|
||||
# Using reverse() keeps these URLs correct if route patterns change.
|
||||
static_route_names: list[str] = [
|
||||
"core:dashboard",
|
||||
# "core:search",
|
||||
"core:dataset_backups",
|
||||
"core:docs_rss",
|
||||
# Core RSS/Atom feeds
|
||||
# "core:campaign_feed",
|
||||
# "core:game_feed",
|
||||
# "core:organization_feed",
|
||||
# "core:reward_campaign_feed",
|
||||
# "core:campaign_feed_atom",
|
||||
# "core:game_feed_atom",
|
||||
# "core:organization_feed_atom",
|
||||
# "core:reward_campaign_feed_atom",
|
||||
# "core:campaign_feed_discord",
|
||||
# "core:game_feed_discord",
|
||||
# "core:organization_feed_discord",
|
||||
# "core:reward_campaign_feed_discord",
|
||||
# Twitch pages
|
||||
"twitch:dashboard",
|
||||
"twitch:badge_list",
|
||||
"twitch:campaign_list",
|
||||
"twitch:channel_list",
|
||||
"twitch:emote_gallery",
|
||||
"twitch:games_grid",
|
||||
"twitch:games_list",
|
||||
"twitch:org_list",
|
||||
"twitch:reward_campaign_list",
|
||||
# Kick pages
|
||||
"kick:dashboard",
|
||||
"kick:campaign_list",
|
||||
"kick:game_list",
|
||||
"kick:category_list",
|
||||
"kick:organization_list",
|
||||
# Kick RSS/Atom feeds
|
||||
# "kick:campaign_feed",
|
||||
# "kick:game_feed",
|
||||
# "kick:category_feed",
|
||||
# "kick:organization_feed",
|
||||
# "kick:campaign_feed_atom",
|
||||
# "kick:game_feed_atom",
|
||||
# "kick:category_feed_atom",
|
||||
# "kick:organization_feed_atom",
|
||||
# "kick:campaign_feed_discord",
|
||||
# "kick:game_feed_discord",
|
||||
# "kick:category_feed_discord",
|
||||
# "kick:organization_feed_discord",
|
||||
# YouTube
|
||||
"youtube:index",
|
||||
# `request` is unused but required by Django's view signature.
|
||||
base_url: str = _build_base_url()
|
||||
sitemap_urls: list[dict[str, str]] = [
|
||||
{"loc": f"{base_url}/"},
|
||||
{"loc": f"{base_url}/twitch/"},
|
||||
{"loc": f"{base_url}/twitch/campaigns/"},
|
||||
{"loc": f"{base_url}/twitch/games/"},
|
||||
{"loc": f"{base_url}/kick/"},
|
||||
{"loc": f"{base_url}/youtube/"},
|
||||
{"loc": f"{base_url}/about/"},
|
||||
{"loc": f"{base_url}/robots.txt"},
|
||||
]
|
||||
|
||||
sitemap_urls: list[dict[str, str]] = []
|
||||
for route_name in static_route_names:
|
||||
url = reverse(route_name)
|
||||
sitemap_urls.append({"url": f"{base_url}{url}"})
|
||||
|
||||
xml_content: str = _render_urlset_xml(sitemap_urls)
|
||||
return HttpResponse(xml_content, content_type="application/xml")
|
||||
|
||||
|
|
@ -323,14 +281,15 @@ def sitemap_twitch_channels_view(request: HttpRequest) -> HttpResponse:
|
|||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
# `request` is unused but required by Django's view signature.
|
||||
base_url: str = _build_base_url()
|
||||
sitemap_urls: list[dict[str, str]] = []
|
||||
|
||||
channels: QuerySet[Channel] = Channel.objects.all()
|
||||
for channel in channels:
|
||||
resource_url: str = reverse("twitch:channel_detail", args=[channel.twitch_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str] = {"url": full_url}
|
||||
entry: dict[str, str] = {"loc": full_url}
|
||||
if channel.updated_at:
|
||||
entry["lastmod"] = channel.updated_at.isoformat()
|
||||
sitemap_urls.append(entry)
|
||||
|
|
@ -349,7 +308,8 @@ def sitemap_twitch_drops_view(request: HttpRequest) -> HttpResponse:
|
|||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
# `request` is unused but required by Django's view signature.
|
||||
base_url: str = _build_base_url()
|
||||
sitemap_urls: list[dict[str, str]] = []
|
||||
|
||||
campaigns: QuerySet[DropCampaign] = DropCampaign.objects.filter(
|
||||
|
|
@ -358,7 +318,7 @@ def sitemap_twitch_drops_view(request: HttpRequest) -> HttpResponse:
|
|||
for campaign in campaigns:
|
||||
resource_url: str = reverse("twitch:campaign_detail", args=[campaign.twitch_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
campaign_url_entry: dict[str, str] = {"url": full_url}
|
||||
campaign_url_entry: dict[str, str] = {"loc": full_url}
|
||||
if campaign.updated_at:
|
||||
campaign_url_entry["lastmod"] = campaign.updated_at.isoformat()
|
||||
sitemap_urls.append(campaign_url_entry)
|
||||
|
|
@ -371,7 +331,7 @@ def sitemap_twitch_drops_view(request: HttpRequest) -> HttpResponse:
|
|||
)
|
||||
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
reward_campaign_url_entry: dict[str, str] = {"url": full_url}
|
||||
reward_campaign_url_entry: dict[str, str] = {"loc": full_url}
|
||||
if reward_campaign.updated_at:
|
||||
reward_campaign_url_entry["lastmod"] = (
|
||||
reward_campaign.updated_at.isoformat()
|
||||
|
|
@ -393,14 +353,15 @@ def sitemap_twitch_others_view(request: HttpRequest) -> HttpResponse:
|
|||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
# `request` is unused but required by Django's view signature.
|
||||
base_url: str = _build_base_url()
|
||||
sitemap_urls: list[dict[str, str]] = []
|
||||
|
||||
games: QuerySet[Game] = Game.objects.all()
|
||||
for game in games:
|
||||
resource_url: str = reverse("twitch:game_detail", args=[game.twitch_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str] = {"url": full_url}
|
||||
entry: dict[str, str] = {"loc": full_url}
|
||||
|
||||
if game.updated_at:
|
||||
entry["lastmod"] = game.updated_at.isoformat()
|
||||
|
|
@ -411,7 +372,7 @@ def sitemap_twitch_others_view(request: HttpRequest) -> HttpResponse:
|
|||
for org in orgs:
|
||||
resource_url: str = reverse("twitch:organization_detail", args=[org.twitch_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str] = {"url": full_url}
|
||||
entry: dict[str, str] = {"loc": full_url}
|
||||
|
||||
if org.updated_at:
|
||||
entry["lastmod"] = org.updated_at.isoformat()
|
||||
|
|
@ -422,10 +383,10 @@ def sitemap_twitch_others_view(request: HttpRequest) -> HttpResponse:
|
|||
for badge_set in badge_sets:
|
||||
resource_url = reverse("twitch:badge_set_detail", args=[badge_set.set_id])
|
||||
full_url = f"{base_url}{resource_url}"
|
||||
sitemap_urls.append({"url": full_url})
|
||||
sitemap_urls.append({"loc": full_url})
|
||||
|
||||
# Emotes currently don't have individual detail pages, but keep a listing here.
|
||||
sitemap_urls.append({"url": f"{base_url}/emotes/"})
|
||||
sitemap_urls.append({"loc": f"{base_url}/emotes/"})
|
||||
|
||||
xml_content: str = _render_urlset_xml(sitemap_urls)
|
||||
return HttpResponse(xml_content, content_type="application/xml")
|
||||
|
|
@ -441,7 +402,8 @@ def sitemap_kick_view(request: HttpRequest) -> HttpResponse:
|
|||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
# `request` is unused but required by Django's view signature.
|
||||
base_url: str = _build_base_url()
|
||||
sitemap_urls: list[dict[str, str]] = []
|
||||
|
||||
kick_campaigns: QuerySet[KickDropCampaign] = KickDropCampaign.objects.filter(
|
||||
|
|
@ -450,19 +412,11 @@ def sitemap_kick_view(request: HttpRequest) -> HttpResponse:
|
|||
for campaign in kick_campaigns:
|
||||
resource_url: str = reverse("kick:campaign_detail", args=[campaign.kick_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str] = {"url": full_url}
|
||||
|
||||
entry: dict[str, str] = {"loc": full_url}
|
||||
if campaign.updated_at:
|
||||
entry["lastmod"] = campaign.updated_at.isoformat()
|
||||
|
||||
sitemap_urls.append(entry)
|
||||
|
||||
# Include Kick organizations and game list pages
|
||||
sitemap_urls.extend((
|
||||
{"url": f"{base_url}/kick/organizations/"},
|
||||
{"url": f"{base_url}/kick/games/"},
|
||||
))
|
||||
|
||||
xml_content: str = _render_urlset_xml(sitemap_urls)
|
||||
return HttpResponse(xml_content, content_type="application/xml")
|
||||
|
||||
|
|
@ -477,10 +431,10 @@ def sitemap_youtube_view(request: HttpRequest) -> HttpResponse:
|
|||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
|
||||
# `request` is unused but required by Django's view signature.
|
||||
base_url: str = _build_base_url()
|
||||
sitemap_urls: list[dict[str, str]] = [
|
||||
{"url": f"{base_url}{reverse('youtube:index')}"},
|
||||
{"loc": f"{base_url}{reverse('youtube:index')}"},
|
||||
]
|
||||
|
||||
xml_content: str = _render_urlset_xml(sitemap_urls)
|
||||
|
|
@ -775,7 +729,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
|
|||
|
||||
|
||||
def dataset_backup_download_view(
|
||||
request: HttpRequest, # noqa: ARG001
|
||||
request: HttpRequest,
|
||||
relative_path: str,
|
||||
) -> FileResponse:
|
||||
"""Download a dataset backup from the data directory.
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ dev = [
|
|||
[tool.pytest.ini_options]
|
||||
DJANGO_SETTINGS_MODULE = "config.settings"
|
||||
python_files = ["test_*.py", "*_test.py"]
|
||||
addopts = "-n 4"
|
||||
addopts = ""
|
||||
filterwarnings = [
|
||||
"ignore:Parsing dates involving a day of month without a year specified is ambiguous:DeprecationWarning",
|
||||
]
|
||||
|
|
@ -87,6 +87,7 @@ lint.ignore = [
|
|||
"PLR0912", # Checks for functions or methods with too many branches, including (nested) if, elif, and else branches, for loops, try-except clauses, and match and case statements.
|
||||
"PLR6301", # Checks for the presence of unused self parameter in methods definitions.
|
||||
"RUF012", # Checks for mutable default values in class attributes.
|
||||
"ARG001", # Checks for the presence of unused arguments in function definitions.
|
||||
|
||||
# Conflicting lint rules when using Ruff's formatter
|
||||
# https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import logging
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
import auto_prefetch
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
|
@ -64,7 +65,7 @@ class Organization(auto_prefetch.Model):
|
|||
def feed_description(self: Organization) -> str:
|
||||
"""Return a description of the organization for RSS feeds."""
|
||||
name: str = self.name or "Unknown Organization"
|
||||
url: str = reverse("twitch:organization_detail", args=[self.twitch_id])
|
||||
url: str = f"{settings.BASE_URL}{reverse('twitch:organization_detail', args=[self.twitch_id])}"
|
||||
|
||||
return format_html(
|
||||
'<p>New Twitch organization added to TTVDrops:</p>\n<p><a href="{}">{}</a></p>',
|
||||
|
|
|
|||
|
|
@ -177,11 +177,12 @@ class RSSFeedTestCase(TestCase):
|
|||
assert 'rel="self"' in content, msg
|
||||
|
||||
msg: str = f"Expected self link to point to campaign feed URL, got: {content}"
|
||||
assert 'href="http://testserver/atom/campaigns/"' in content, msg
|
||||
assert 'href="https://ttvdrops.lovinator.space/atom/campaigns/"' in content, msg
|
||||
|
||||
msg: str = f"Expected entry ID to be the campaign URL, got: {content}"
|
||||
assert (
|
||||
"<id>http://testserver/twitch/campaigns/test-campaign-123/</id>" in content
|
||||
"<id>https://ttvdrops.lovinator.space/twitch/campaigns/test-campaign-123/</id>"
|
||||
in content
|
||||
), msg
|
||||
|
||||
def test_all_atom_feeds_use_url_ids_and_correct_self_links(self) -> None:
|
||||
|
|
@ -190,27 +191,27 @@ class RSSFeedTestCase(TestCase):
|
|||
(
|
||||
"core:campaign_feed_atom",
|
||||
{},
|
||||
f"http://testserver{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}",
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}",
|
||||
),
|
||||
(
|
||||
"core:game_feed_atom",
|
||||
{},
|
||||
f"http://testserver{reverse('twitch:game_detail', args=[self.game.twitch_id])}",
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:game_detail', args=[self.game.twitch_id])}",
|
||||
),
|
||||
(
|
||||
"core:game_campaign_feed_atom",
|
||||
{"twitch_id": self.game.twitch_id},
|
||||
f"http://testserver{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}",
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}",
|
||||
),
|
||||
(
|
||||
"core:organization_feed_atom",
|
||||
{},
|
||||
f"http://testserver{reverse('twitch:organization_detail', args=[self.org.twitch_id])}",
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:organization_detail', args=[self.org.twitch_id])}",
|
||||
),
|
||||
(
|
||||
"core:reward_campaign_feed_atom",
|
||||
{},
|
||||
f"http://testserver{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}",
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}",
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -221,7 +222,7 @@ class RSSFeedTestCase(TestCase):
|
|||
assert response.status_code == 200
|
||||
content: str = response.content.decode("utf-8")
|
||||
|
||||
expected_self_link: str = f'href="http://testserver{url}"'
|
||||
expected_self_link: str = f'href="https://ttvdrops.lovinator.space{url}"'
|
||||
msg: str = f"Expected self link in Atom feed {url_name}, got: {content}"
|
||||
assert 'rel="self"' in content, msg
|
||||
|
||||
|
|
@ -317,7 +318,7 @@ class RSSFeedTestCase(TestCase):
|
|||
msg: str = (
|
||||
f"Expected absolute media enclosure URLs for {url}, got: {content}"
|
||||
)
|
||||
assert "http://testserver/media/" in content, msg
|
||||
assert "https://ttvdrops.lovinator.space/media/" in content, msg
|
||||
assert 'url="/media/' not in content, msg
|
||||
assert 'href="/media/' not in content, msg
|
||||
|
||||
|
|
@ -1321,27 +1322,27 @@ class DiscordFeedTestCase(TestCase):
|
|||
(
|
||||
"core:campaign_feed_discord",
|
||||
{},
|
||||
f"http://testserver{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}",
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}",
|
||||
),
|
||||
(
|
||||
"core:game_feed_discord",
|
||||
{},
|
||||
f"http://testserver{reverse('twitch:game_detail', args=[self.game.twitch_id])}",
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:game_detail', args=[self.game.twitch_id])}",
|
||||
),
|
||||
(
|
||||
"core:game_campaign_feed_discord",
|
||||
{"twitch_id": self.game.twitch_id},
|
||||
f"http://testserver{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}",
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}",
|
||||
),
|
||||
(
|
||||
"core:organization_feed_discord",
|
||||
{},
|
||||
f"http://testserver{reverse('twitch:organization_detail', args=[self.org.twitch_id])}",
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:organization_detail', args=[self.org.twitch_id])}",
|
||||
),
|
||||
(
|
||||
"core:reward_campaign_feed_discord",
|
||||
{},
|
||||
f"http://testserver{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}",
|
||||
f"https://ttvdrops.lovinator.space{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}",
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -1352,7 +1353,7 @@ class DiscordFeedTestCase(TestCase):
|
|||
assert response.status_code == 200
|
||||
content: str = response.content.decode("utf-8")
|
||||
|
||||
expected_self_link: str = f'href="http://testserver{url}"'
|
||||
expected_self_link: str = f'href="https://ttvdrops.lovinator.space{url}"'
|
||||
msg: str = f"Expected self link in Discord feed {url_name}, got: {content}"
|
||||
assert 'rel="self"' in content, msg
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ if TYPE_CHECKING:
|
|||
from twitch.views import Page
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def apply_base_url_override(settings: object) -> None:
|
||||
"""Ensure BASE_URL is globally overridden for all tests."""
|
||||
settings.BASE_URL = "https://ttvdrops.lovinator.space"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSearchView:
|
||||
"""Tests for the search_view function."""
|
||||
|
|
@ -1562,14 +1568,14 @@ class TestSitemapView:
|
|||
|
||||
# Check for the homepage and a few key list views across apps.
|
||||
assert (
|
||||
"<loc>http://testserver/</loc>" in content
|
||||
"<loc>https://ttvdrops.lovinator.space/</loc>" in content
|
||||
or "<loc>http://localhost:8000/</loc>" in content
|
||||
)
|
||||
assert "http://testserver/twitch/" in content
|
||||
assert "http://testserver/kick/" in content
|
||||
assert "http://testserver/youtube/" in content
|
||||
assert "http://testserver/twitch/campaigns/" in content
|
||||
assert "http://testserver/twitch/games/" in content
|
||||
assert "https://ttvdrops.lovinator.space/twitch/" in content
|
||||
assert "https://ttvdrops.lovinator.space/kick/" in content
|
||||
assert "https://ttvdrops.lovinator.space/youtube/" in content
|
||||
assert "https://ttvdrops.lovinator.space/twitch/campaigns/" in content
|
||||
assert "https://ttvdrops.lovinator.space/twitch/games/" in content
|
||||
|
||||
def test_sitemap_contains_game_detail_pages(
|
||||
self,
|
||||
|
|
@ -1605,13 +1611,13 @@ class TestSitemapView:
|
|||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-drops.xml")
|
||||
content: str = response.content.decode()
|
||||
|
||||
active_loc: str = f"<loc>http://testserver/twitch/campaigns/{active_campaign.twitch_id}/</loc>"
|
||||
active_loc: str = f"<loc>https://ttvdrops.lovinator.space/twitch/campaigns/{active_campaign.twitch_id}/</loc>"
|
||||
active_index: int = content.find(active_loc)
|
||||
assert active_index != -1
|
||||
active_end: int = content.find("</url>", active_index)
|
||||
assert active_end != -1
|
||||
|
||||
inactive_loc: str = f"<loc>http://testserver/twitch/campaigns/{inactive_campaign.twitch_id}/</loc>"
|
||||
inactive_loc: str = f"<loc>https://ttvdrops.lovinator.space/twitch/campaigns/{inactive_campaign.twitch_id}/</loc>"
|
||||
inactive_index: int = content.find(inactive_loc)
|
||||
assert inactive_index != -1
|
||||
inactive_end: int = content.find("</url>", inactive_index)
|
||||
|
|
@ -1629,17 +1635,13 @@ class TestSitemapView:
|
|||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-kick.xml")
|
||||
content: str = response.content.decode()
|
||||
|
||||
active_loc: str = (
|
||||
f"<loc>http://testserver/kick/campaigns/{active_campaign.kick_id}/</loc>"
|
||||
)
|
||||
active_loc: str = f"<loc>https://ttvdrops.lovinator.space/kick/campaigns/{active_campaign.kick_id}/</loc>"
|
||||
active_index: int = content.find(active_loc)
|
||||
assert active_index != -1
|
||||
active_end: int = content.find("</url>", active_index)
|
||||
assert active_end != -1
|
||||
|
||||
inactive_loc: str = (
|
||||
f"<loc>http://testserver/kick/campaigns/{inactive_campaign.kick_id}/</loc>"
|
||||
)
|
||||
inactive_loc: str = f"<loc>https://ttvdrops.lovinator.space/kick/campaigns/{inactive_campaign.kick_id}/</loc>"
|
||||
inactive_index: int = content.find(inactive_loc)
|
||||
assert inactive_index != -1
|
||||
inactive_end: int = content.find("</url>", inactive_index)
|
||||
|
|
@ -1972,7 +1974,7 @@ class TestImageObjectStructuredData:
|
|||
assert img["creator"] == {
|
||||
"@type": "Organization",
|
||||
"name": org.name,
|
||||
"url": f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}",
|
||||
"url": f"https://ttvdrops.lovinator.space{reverse('twitch:organization_detail', args=[org.twitch_id])}",
|
||||
}
|
||||
|
||||
def test_game_schema_no_image_when_no_box_art(
|
||||
|
|
@ -2084,7 +2086,7 @@ class TestImageObjectStructuredData:
|
|||
assert img["creator"] == {
|
||||
"@type": "Organization",
|
||||
"name": org.name,
|
||||
"url": f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}",
|
||||
"url": f"https://ttvdrops.lovinator.space{reverse('twitch:organization_detail', args=[org.twitch_id])}",
|
||||
}
|
||||
|
||||
def test_campaign_schema_no_image_when_no_image_url(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue