diff --git a/config/settings.py b/config/settings.py index 8a6519c..0e7026f 100644 --- a/config/settings.py +++ b/config/settings.py @@ -228,92 +228,3 @@ 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 diff --git a/config/tests/test_seo.py b/config/tests/test_seo.py index 86f3e85..3574eb1 100644 --- a/config/tests/test_seo.py +++ b/config/tests/test_seo.py @@ -48,33 +48,23 @@ 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") - == "https://ttvdrops.lovinator.space/drops/" - ) - assert ( - '' - in content - ) + assert _extract_meta_content(content, "og:url") == "http://testserver/drops/" + assert '' 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://ttvdrops.lovinator.space/custom-page/", + "page_url": "https://example.com/custom-page/", }, path="/ignored/", ) assert ( - _extract_meta_content(content, "og:url") - == "https://ttvdrops.lovinator.space/custom-page/" - ) - assert ( - '' - in content + _extract_meta_content(content, "og:url") == "https://example.com/custom-page/" ) + assert '' in content def test_meta_tags_twitter_card_is_summary_without_image() -> None: @@ -88,19 +78,16 @@ 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://ttvdrops.lovinator.space/image.png", + "page_image": "https://example.com/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://ttvdrops.lovinator.space/image.png" - ) + assert _extract_meta_content(content, "og:image") == "https://example.com/image.png" assert ( _extract_meta_content(content, "twitter:image") - == "https://ttvdrops.lovinator.space/image.png" + == "https://example.com/image.png" ) assert _extract_meta_content(content, "og:image:width") == "1200" assert _extract_meta_content(content, "og:image:height") == "630" @@ -110,14 +97,10 @@ 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://ttvdrops.lovinator.space/page/1/"}, - {"rel": "next", "url": "https://ttvdrops.lovinator.space/page/3/"}, + {"rel": "prev", "url": "https://example.com/page/1/"}, + {"rel": "next", "url": "https://example.com/page/3/"}, ], }) - assert ( - '' in content - ) - assert ( - '' in content - ) + assert '' in content + assert '' in content diff --git a/core/tests/test_sitemaps.py b/core/tests/test_sitemaps.py deleted file mode 100644 index 4486791..0000000 --- a/core/tests/test_sitemaps.py +++ /dev/null @@ -1,73 +0,0 @@ -import xml.etree.ElementTree as ET # noqa: S405 -from typing import TYPE_CHECKING - -from django.urls import reverse - -if TYPE_CHECKING: - from django.test.client import Client - - -def _extract_locs(xml_bytes: bytes) -> list[str]: - root = ET.fromstring(xml_bytes) # noqa: S314 - ns = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"} - return [el.text for el in root.findall(".//s:loc", ns) if el.text] - - -def test_sitemap_static_contains_expected_links( - client: Client, - settings: object, -) -> None: - """Ensure the static sitemap contains the main site links across apps. - - This test checks a representative set of URLs from core, twitch, kick, and - youtube apps as well as some misc static files like /robots.txt. - """ - # Ensure deterministic BASE_URL - settings.BASE_URL = "https://ttvdrops.lovinator.space" - - response = client.get(reverse("sitemap-static")) - assert response.status_code == 200 - assert response["Content-Type"] == "application/xml" - - locs = _extract_locs(response.content) - - base = settings.BASE_URL.rstrip("/") - - expected_paths = [ - reverse("core:dashboard"), - reverse("core:search"), - reverse("core:debug"), - reverse("core:dataset_backups"), - reverse("core:docs_rss"), - reverse("core:campaign_feed"), - reverse("core:game_feed"), - reverse("core:organization_feed"), - reverse("core:reward_campaign_feed"), - reverse("core:campaign_feed_atom"), - reverse("core:campaign_feed_discord"), - reverse("twitch:dashboard"), - reverse("twitch:campaign_list"), - reverse("twitch:games_grid"), - reverse("twitch:games_list"), - reverse("twitch:channel_list"), - reverse("twitch:badge_list"), - reverse("twitch:emote_gallery"), - reverse("twitch:org_list"), - reverse("twitch:reward_campaign_list"), - reverse("twitch:export_campaigns_csv"), - reverse("kick:dashboard"), - reverse("kick:campaign_list"), - reverse("kick:game_list"), - reverse("kick:organization_list"), - reverse("kick:campaign_feed"), - reverse("kick:game_feed"), - reverse("kick:organization_feed"), - reverse("youtube:index"), - "/about/", - "/robots.txt", - ] - - expected: set[str] = {base + p for p in expected_paths} - - for url in expected: - assert url in locs, f"Expected {url} in sitemap, found {len(locs)} entries" diff --git a/core/tests/test_views.py b/core/tests/test_views.py deleted file mode 100644 index bf9327c..0000000 --- a/core/tests/test_views.py +++ /dev/null @@ -1,36 +0,0 @@ -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 " 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 " 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 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") diff --git a/core/views.py b/core/views.py index 464ded6..4c3000c 100644 --- a/core/views.py +++ b/core/views.py @@ -15,15 +15,14 @@ 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 @@ -89,9 +88,6 @@ 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 @@ -140,13 +136,11 @@ def _render_urlset_xml( xml += '\n' for url_entry in url_entries: xml += " \n" - loc = url_entry.get("loc") or url_entry.get("url") # Handle both keys - if loc: - xml += f" {loc}\n" - if "lastmod" in url_entry: + xml += f" {url_entry['url']}\n" + if url_entry.get("lastmod"): xml += f" {url_entry['lastmod']}\n" xml += " \n" - xml += "\n" + xml += "" return xml @@ -171,9 +165,9 @@ def _render_sitemap_index_xml(sitemap_entries: list[dict[str, str]]) -> str: return xml -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") +def _build_base_url(request: HttpRequest) -> str: + """Return base url including scheme and host.""" + return f"{request.scheme}://{request.get_host()}" # MARK: /sitemap.xml @@ -186,7 +180,7 @@ def sitemap_view(request: HttpRequest) -> HttpResponse: Returns: HttpResponse: The rendered sitemap index XML. """ - base_url: str = _build_base_url() + base_url: str = _build_base_url(request) # Compute last modified per-section so search engines can more intelligently crawl. # Do not fabricate a lastmod date if the section has no data. @@ -255,63 +249,66 @@ def sitemap_static_view(request: HttpRequest) -> HttpResponse: Returns: HttpResponse: The rendered sitemap XML. """ - # `request` is unused but required by Django's view signature. - base_url: str = _build_base_url().rstrip("/") - sitemap_urls: list[dict[str, str]] = [ - {"loc": f"{base_url}{reverse('core:dashboard')}"}, - {"loc": f"{base_url}{reverse('core:search')}"}, - {"loc": f"{base_url}{reverse('core:debug')}"}, - {"loc": f"{base_url}{reverse('core:dataset_backups')}"}, - {"loc": f"{base_url}{reverse('core:docs_rss')}"}, - # Core RSS/Atom/Discord feeds - {"loc": f"{base_url}{reverse('core:campaign_feed')}"}, - {"loc": f"{base_url}{reverse('core:game_feed')}"}, - {"loc": f"{base_url}{reverse('core:organization_feed')}"}, - {"loc": f"{base_url}{reverse('core:reward_campaign_feed')}"}, - {"loc": f"{base_url}{reverse('core:campaign_feed_atom')}"}, - {"loc": f"{base_url}{reverse('core:game_feed_atom')}"}, - {"loc": f"{base_url}{reverse('core:organization_feed_atom')}"}, - {"loc": f"{base_url}{reverse('core:reward_campaign_feed_atom')}"}, - {"loc": f"{base_url}{reverse('core:campaign_feed_discord')}"}, - {"loc": f"{base_url}{reverse('core:game_feed_discord')}"}, - {"loc": f"{base_url}{reverse('core:organization_feed_discord')}"}, - {"loc": f"{base_url}{reverse('core:reward_campaign_feed_discord')}"}, - # Twitch app pages - {"loc": f"{base_url}{reverse('twitch:dashboard')}"}, - {"loc": f"{base_url}{reverse('twitch:campaign_list')}"}, - {"loc": f"{base_url}{reverse('twitch:games_grid')}"}, - {"loc": f"{base_url}{reverse('twitch:games_list')}"}, - {"loc": f"{base_url}{reverse('twitch:channel_list')}"}, - {"loc": f"{base_url}{reverse('twitch:badge_list')}"}, - {"loc": f"{base_url}{reverse('twitch:emote_gallery')}"}, - {"loc": f"{base_url}{reverse('twitch:org_list')}"}, - {"loc": f"{base_url}{reverse('twitch:reward_campaign_list')}"}, - {"loc": f"{base_url}{reverse('twitch:export_campaigns_csv')}"}, - {"loc": f"{base_url}{reverse('twitch:export_games_csv')}"}, - {"loc": f"{base_url}{reverse('twitch:export_organizations_csv')}"}, - {"loc": f"{base_url}{reverse('twitch:export_campaigns_json')}"}, - {"loc": f"{base_url}{reverse('twitch:export_games_json')}"}, - {"loc": f"{base_url}{reverse('twitch:export_organizations_json')}"}, - # Kick app pages and feeds - {"loc": f"{base_url}{reverse('kick:dashboard')}"}, - {"loc": f"{base_url}{reverse('kick:campaign_list')}"}, - {"loc": f"{base_url}{reverse('kick:game_list')}"}, - {"loc": f"{base_url}{reverse('kick:organization_list')}"}, - {"loc": f"{base_url}{reverse('kick:campaign_feed')}"}, - {"loc": f"{base_url}{reverse('kick:game_feed')}"}, - {"loc": f"{base_url}{reverse('kick:organization_feed')}"}, - {"loc": f"{base_url}{reverse('kick:campaign_feed_atom')}"}, - {"loc": f"{base_url}{reverse('kick:game_feed_atom')}"}, - {"loc": f"{base_url}{reverse('kick:organization_feed_atom')}"}, - {"loc": f"{base_url}{reverse('kick:campaign_feed_discord')}"}, - {"loc": f"{base_url}{reverse('kick:game_feed_discord')}"}, - {"loc": f"{base_url}{reverse('kick:organization_feed_discord')}"}, + 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 - {"loc": f"{base_url}{reverse('youtube:index')}"}, - # Misc/static - {"loc": f"{base_url}/about/"}, - {"loc": f"{base_url}/robots.txt"}, + "youtube:index", ] + + 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") @@ -326,15 +323,14 @@ def sitemap_twitch_channels_view(request: HttpRequest) -> HttpResponse: Returns: HttpResponse: The rendered sitemap XML. """ - # `request` is unused but required by Django's view signature. - base_url: str = _build_base_url() + base_url: str = _build_base_url(request) 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] = {"loc": full_url} + entry: dict[str, str] = {"url": full_url} if channel.updated_at: entry["lastmod"] = channel.updated_at.isoformat() sitemap_urls.append(entry) @@ -353,8 +349,7 @@ def sitemap_twitch_drops_view(request: HttpRequest) -> HttpResponse: Returns: HttpResponse: The rendered sitemap XML. """ - # `request` is unused but required by Django's view signature. - base_url: str = _build_base_url() + base_url: str = _build_base_url(request) sitemap_urls: list[dict[str, str]] = [] campaigns: QuerySet[DropCampaign] = DropCampaign.objects.filter( @@ -363,7 +358,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] = {"loc": full_url} + campaign_url_entry: dict[str, str] = {"url": full_url} if campaign.updated_at: campaign_url_entry["lastmod"] = campaign.updated_at.isoformat() sitemap_urls.append(campaign_url_entry) @@ -376,7 +371,7 @@ def sitemap_twitch_drops_view(request: HttpRequest) -> HttpResponse: ) full_url: str = f"{base_url}{resource_url}" - reward_campaign_url_entry: dict[str, str] = {"loc": full_url} + reward_campaign_url_entry: dict[str, str] = {"url": full_url} if reward_campaign.updated_at: reward_campaign_url_entry["lastmod"] = ( reward_campaign.updated_at.isoformat() @@ -398,15 +393,14 @@ def sitemap_twitch_others_view(request: HttpRequest) -> HttpResponse: Returns: HttpResponse: The rendered sitemap XML. """ - # `request` is unused but required by Django's view signature. - base_url: str = _build_base_url() + base_url: str = _build_base_url(request) 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] = {"loc": full_url} + entry: dict[str, str] = {"url": full_url} if game.updated_at: entry["lastmod"] = game.updated_at.isoformat() @@ -417,7 +411,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] = {"loc": full_url} + entry: dict[str, str] = {"url": full_url} if org.updated_at: entry["lastmod"] = org.updated_at.isoformat() @@ -428,10 +422,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({"loc": full_url}) + sitemap_urls.append({"url": full_url}) # Emotes currently don't have individual detail pages, but keep a listing here. - sitemap_urls.append({"loc": f"{base_url}/emotes/"}) + sitemap_urls.append({"url": f"{base_url}/emotes/"}) xml_content: str = _render_urlset_xml(sitemap_urls) return HttpResponse(xml_content, content_type="application/xml") @@ -447,8 +441,7 @@ def sitemap_kick_view(request: HttpRequest) -> HttpResponse: Returns: HttpResponse: The rendered sitemap XML. """ - # `request` is unused but required by Django's view signature. - base_url: str = _build_base_url() + base_url: str = _build_base_url(request) sitemap_urls: list[dict[str, str]] = [] kick_campaigns: QuerySet[KickDropCampaign] = KickDropCampaign.objects.filter( @@ -457,11 +450,19 @@ 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] = {"loc": full_url} + entry: dict[str, str] = {"url": 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") @@ -476,10 +477,10 @@ def sitemap_youtube_view(request: HttpRequest) -> HttpResponse: Returns: HttpResponse: The rendered sitemap XML. """ - # `request` is unused but required by Django's view signature. - base_url: str = _build_base_url() + base_url: str = _build_base_url(request) + sitemap_urls: list[dict[str, str]] = [ - {"loc": f"{base_url}{reverse('youtube:index')}"}, + {"url": f"{base_url}{reverse('youtube:index')}"}, ] xml_content: str = _render_urlset_xml(sitemap_urls) @@ -774,7 +775,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse: def dataset_backup_download_view( - request: HttpRequest, + request: HttpRequest, # noqa: ARG001 relative_path: str, ) -> FileResponse: """Download a dataset backup from the data directory. diff --git a/pyproject.toml b/pyproject.toml index 28e688f..8a42e43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dev = [ [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings" python_files = ["test_*.py", "*_test.py"] -addopts = "" +addopts = "-n 4" filterwarnings = [ "ignore:Parsing dates involving a day of month without a year specified is ambiguous:DeprecationWarning", ] @@ -87,7 +87,6 @@ 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 diff --git a/templates/core/dashboard.html b/templates/core/dashboard.html index b634498..44155b8 100644 --- a/templates/core/dashboard.html +++ b/templates/core/dashboard.html @@ -140,18 +140,6 @@ ... and {{ campaign_data.allowed_channels|length|add:"-5" }} more {% endif %} - {% else %} - {% if campaign_data.campaign.game.twitch_directory_url %} -
  • - - Go to a participating live channel - -
  • - {% else %} -
  • Failed to get Twitch directory URL :(
  • - {% endif %} {% endif %} diff --git a/twitch/models.py b/twitch/models.py index 1ed2a57..6e85870 100644 --- a/twitch/models.py +++ b/twitch/models.py @@ -2,7 +2,6 @@ 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 @@ -65,7 +64,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 = f"{settings.BASE_URL}{reverse('twitch:organization_detail', args=[self.twitch_id])}" + url: str = reverse("twitch:organization_detail", args=[self.twitch_id]) return format_html( '

    New Twitch organization added to TTVDrops:

    \n

    {}

    ', diff --git a/twitch/tests/test_feeds.py b/twitch/tests/test_feeds.py index b69e9cb..cb1fdec 100644 --- a/twitch/tests/test_feeds.py +++ b/twitch/tests/test_feeds.py @@ -177,12 +177,11 @@ 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="https://ttvdrops.lovinator.space/atom/campaigns/"' in content, msg + assert 'href="http://testserver/atom/campaigns/"' in content, msg msg: str = f"Expected entry ID to be the campaign URL, got: {content}" assert ( - "https://ttvdrops.lovinator.space/twitch/campaigns/test-campaign-123/" - in content + "http://testserver/twitch/campaigns/test-campaign-123/" in content ), msg def test_all_atom_feeds_use_url_ids_and_correct_self_links(self) -> None: @@ -191,27 +190,27 @@ class RSSFeedTestCase(TestCase): ( "core:campaign_feed_atom", {}, - f"https://ttvdrops.lovinator.space{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}", + f"http://testserver{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}", ), ( "core:game_feed_atom", {}, - f"https://ttvdrops.lovinator.space{reverse('twitch:game_detail', args=[self.game.twitch_id])}", + f"http://testserver{reverse('twitch:game_detail', args=[self.game.twitch_id])}", ), ( "core:game_campaign_feed_atom", {"twitch_id": self.game.twitch_id}, - f"https://ttvdrops.lovinator.space{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}", + f"http://testserver{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}", ), ( "core:organization_feed_atom", {}, - f"https://ttvdrops.lovinator.space{reverse('twitch:organization_detail', args=[self.org.twitch_id])}", + f"http://testserver{reverse('twitch:organization_detail', args=[self.org.twitch_id])}", ), ( "core:reward_campaign_feed_atom", {}, - f"https://ttvdrops.lovinator.space{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}", + f"http://testserver{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}", ), ] @@ -222,7 +221,7 @@ class RSSFeedTestCase(TestCase): assert response.status_code == 200 content: str = response.content.decode("utf-8") - expected_self_link: str = f'href="https://ttvdrops.lovinator.space{url}"' + expected_self_link: str = f'href="http://testserver{url}"' msg: str = f"Expected self link in Atom feed {url_name}, got: {content}" assert 'rel="self"' in content, msg @@ -318,7 +317,7 @@ class RSSFeedTestCase(TestCase): msg: str = ( f"Expected absolute media enclosure URLs for {url}, got: {content}" ) - assert "https://ttvdrops.lovinator.space/media/" in content, msg + assert "http://testserver/media/" in content, msg assert 'url="/media/' not in content, msg assert 'href="/media/' not in content, msg @@ -1322,27 +1321,27 @@ class DiscordFeedTestCase(TestCase): ( "core:campaign_feed_discord", {}, - f"https://ttvdrops.lovinator.space{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}", + f"http://testserver{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}", ), ( "core:game_feed_discord", {}, - f"https://ttvdrops.lovinator.space{reverse('twitch:game_detail', args=[self.game.twitch_id])}", + f"http://testserver{reverse('twitch:game_detail', args=[self.game.twitch_id])}", ), ( "core:game_campaign_feed_discord", {"twitch_id": self.game.twitch_id}, - f"https://ttvdrops.lovinator.space{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}", + f"http://testserver{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}", ), ( "core:organization_feed_discord", {}, - f"https://ttvdrops.lovinator.space{reverse('twitch:organization_detail', args=[self.org.twitch_id])}", + f"http://testserver{reverse('twitch:organization_detail', args=[self.org.twitch_id])}", ), ( "core:reward_campaign_feed_discord", {}, - f"https://ttvdrops.lovinator.space{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}", + f"http://testserver{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}", ), ] @@ -1353,7 +1352,7 @@ class DiscordFeedTestCase(TestCase): assert response.status_code == 200 content: str = response.content.decode("utf-8") - expected_self_link: str = f'href="https://ttvdrops.lovinator.space{url}"' + expected_self_link: str = f'href="http://testserver{url}"' msg: str = f"Expected self link in Discord feed {url_name}, got: {content}" assert 'rel="self"' in content, msg diff --git a/twitch/tests/test_views.py b/twitch/tests/test_views.py index 03f67d6..6186662 100644 --- a/twitch/tests/test_views.py +++ b/twitch/tests/test_views.py @@ -41,12 +41,6 @@ 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" # pyright: ignore[reportAttributeAccessIssue] - - @pytest.mark.django_db class TestSearchView: """Tests for the search_view function.""" @@ -1568,14 +1562,14 @@ class TestSitemapView: # Check for the homepage and a few key list views across apps. assert ( - "https://ttvdrops.lovinator.space/" in content + "http://testserver/" in content or "http://localhost:8000/" 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 + 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 def test_sitemap_contains_game_detail_pages( self, @@ -1611,13 +1605,13 @@ class TestSitemapView: response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-drops.xml") content: str = response.content.decode() - active_loc: str = f"https://ttvdrops.lovinator.space/twitch/campaigns/{active_campaign.twitch_id}/" + active_loc: str = f"http://testserver/twitch/campaigns/{active_campaign.twitch_id}/" active_index: int = content.find(active_loc) assert active_index != -1 active_end: int = content.find("", active_index) assert active_end != -1 - inactive_loc: str = f"https://ttvdrops.lovinator.space/twitch/campaigns/{inactive_campaign.twitch_id}/" + inactive_loc: str = f"http://testserver/twitch/campaigns/{inactive_campaign.twitch_id}/" inactive_index: int = content.find(inactive_loc) assert inactive_index != -1 inactive_end: int = content.find("", inactive_index) @@ -1635,13 +1629,17 @@ class TestSitemapView: response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-kick.xml") content: str = response.content.decode() - active_loc: str = f"https://ttvdrops.lovinator.space/kick/campaigns/{active_campaign.kick_id}/" + active_loc: str = ( + f"http://testserver/kick/campaigns/{active_campaign.kick_id}/" + ) active_index: int = content.find(active_loc) assert active_index != -1 active_end: int = content.find("", active_index) assert active_end != -1 - inactive_loc: str = f"https://ttvdrops.lovinator.space/kick/campaigns/{inactive_campaign.kick_id}/" + inactive_loc: str = ( + f"http://testserver/kick/campaigns/{inactive_campaign.kick_id}/" + ) inactive_index: int = content.find(inactive_loc) assert inactive_index != -1 inactive_end: int = content.find("", inactive_index) @@ -1974,7 +1972,7 @@ class TestImageObjectStructuredData: assert img["creator"] == { "@type": "Organization", "name": org.name, - "url": f"https://ttvdrops.lovinator.space{reverse('twitch:organization_detail', args=[org.twitch_id])}", + "url": f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}", } def test_game_schema_no_image_when_no_box_art( @@ -2086,7 +2084,7 @@ class TestImageObjectStructuredData: assert img["creator"] == { "@type": "Organization", "name": org.name, - "url": f"https://ttvdrops.lovinator.space{reverse('twitch:organization_detail', args=[org.twitch_id])}", + "url": f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}", } def test_campaign_schema_no_image_when_no_image_url(