From 8dfcd118e5a90a4b21a5cd70721ceabdbbec3488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Helle=C5=9Ben?= Date: Sun, 22 Mar 2026 00:34:01 +0100 Subject: [PATCH 1/3] Fix pyright attribute access issues in test_sitemaps and fix import in views --- core/tests/test_sitemaps.py | 4 ++-- core/views.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/tests/test_sitemaps.py b/core/tests/test_sitemaps.py index 4486791..c833d26 100644 --- a/core/tests/test_sitemaps.py +++ b/core/tests/test_sitemaps.py @@ -23,7 +23,7 @@ def test_sitemap_static_contains_expected_links( youtube apps as well as some misc static files like /robots.txt. """ # Ensure deterministic BASE_URL - settings.BASE_URL = "https://ttvdrops.lovinator.space" + settings.BASE_URL = "https://ttvdrops.lovinator.space" # pyright: ignore[reportAttributeAccessIssue] response = client.get(reverse("sitemap-static")) assert response.status_code == 200 @@ -31,7 +31,7 @@ def test_sitemap_static_contains_expected_links( locs = _extract_locs(response.content) - base = settings.BASE_URL.rstrip("/") + base = settings.BASE_URL.rstrip("/") # pyright: ignore[reportAttributeAccessIssue] expected_paths = [ reverse("core:dashboard"), diff --git a/core/views.py b/core/views.py index 464ded6..2c6e8b1 100644 --- a/core/views.py +++ b/core/views.py @@ -22,8 +22,8 @@ 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 From 867ddf76768c25f573b129e69329f9f280677715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Helle=C5=9Ben?= Date: Sun, 22 Mar 2026 00:42:55 +0100 Subject: [PATCH 2/3] Update game detail template to improve box art alt text --- templates/twitch/game_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/twitch/game_detail.html b/templates/twitch/game_detail.html index 940b883..31f66af 100644 --- a/templates/twitch/game_detail.html +++ b/templates/twitch/game_detail.html @@ -24,7 +24,7 @@
{% if game.box_art_best_url %} - {% picture game.box_art_best_url alt=game.name width=160 %} + {% picture game.box_art_best_url alt="Box art for "|add:game.get_game_name width=160 %} {% endif %}
From ef00b4c020ae466e4d9b95b7cfc5b60f672fbb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Helle=C5=9Ben?= Date: Sun, 22 Mar 2026 01:12:23 +0100 Subject: [PATCH 3/3] Fix feed links --- pyproject.toml | 18 +++++------ twitch/feeds.py | 4 +-- ...ename_operation_name_to_operation_names.py | 4 +-- .../0016_mark_all_drops_fully_imported.py | 2 +- twitch/tests/test_feeds.py | 30 +++++++++++++++++++ twitch/views.py | 8 ++--- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 28e688f..0b8e299 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,14 +5,20 @@ description = "Get notified when a new drop is available on Twitch." readme = "README.md" requires-python = ">=3.14" dependencies = [ + "celery[redis]", "colorama", "dateparser", "django-auto-prefetch", + "django-celery-beat", + "django-celery-results", "django-debug-toolbar", "django-silk", "django", + "flower", "gunicorn", + "hiredis", "httpx", + "index-now-for-python", "json-repair", "pillow", "platformdirs", @@ -20,17 +26,11 @@ dependencies = [ "pydantic", "pygments", "python-dotenv", + "redis", "sentry-sdk", "setproctitle", - "tqdm", - "index-now-for-python", "sitemap-parser", - "celery[redis]", - "django-celery-results", - "django-celery-beat", - "flower", - "hiredis", - "redis", + "tqdm", ] @@ -41,9 +41,9 @@ dev = [ "hypothesis[django]", "pytest-cov", "pytest-django", + "pytest-randomly", "pytest-xdist[psutil]", "pytest", - "pytest-randomly", ] [tool.pytest.ini_options] diff --git a/twitch/feeds.py b/twitch/feeds.py index 6a8678c..498db75 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -692,7 +692,7 @@ class OrganizationRSSFeed(TTVDropsBaseFeed): feed_type = BrowserFriendlyRss201rev2Feed title: str = "TTVDrops Twitch Organizations" - link: str = "/organizations/" + link: str = "/twitch/organizations/" description: str = "Latest organizations on TTVDrops" _limit: int | None = None @@ -921,7 +921,7 @@ class DropCampaignFeed(TTVDropsBaseFeed): """RSS feed for latest drop campaigns.""" title: str = "Twitch Drop Campaigns" - link: str = "/campaigns/" + link: str = "/" description: str = "Latest Twitch drop campaigns on TTVDrops" item_guid_is_permalink = True diff --git a/twitch/migrations/0007_rename_operation_name_to_operation_names.py b/twitch/migrations/0007_rename_operation_name_to_operation_names.py index 55985e2..63190e1 100644 --- a/twitch/migrations/0007_rename_operation_name_to_operation_names.py +++ b/twitch/migrations/0007_rename_operation_name_to_operation_names.py @@ -5,7 +5,7 @@ from django.db import migrations from django.db import models -def migrate_operation_name_to_list(apps, schema_editor) -> None: # noqa: ANN001, ARG001 +def migrate_operation_name_to_list(apps, schema_editor) -> None: # noqa: ANN001 """Convert operation_name string values to operation_names list.""" DropCampaign = apps.get_model("twitch", "DropCampaign") for campaign in DropCampaign.objects.all(): @@ -14,7 +14,7 @@ def migrate_operation_name_to_list(apps, schema_editor) -> None: # noqa: ANN001 campaign.save(update_fields=["operation_names"]) -def reverse_operation_names_to_string(apps, schema_editor) -> None: # noqa: ARG001, ANN001 +def reverse_operation_names_to_string(apps, schema_editor) -> None: # noqa: ANN001 """Convert operation_names list back to operation_name string (first item only).""" DropCampaign = apps.get_model("twitch", "DropCampaign") for campaign in DropCampaign.objects.all(): diff --git a/twitch/migrations/0016_mark_all_drops_fully_imported.py b/twitch/migrations/0016_mark_all_drops_fully_imported.py index 9fd5630..54b4003 100644 --- a/twitch/migrations/0016_mark_all_drops_fully_imported.py +++ b/twitch/migrations/0016_mark_all_drops_fully_imported.py @@ -1,7 +1,7 @@ from django.db import migrations -def mark_all_drops_fully_imported(apps, schema_editor) -> None: # noqa: ANN001, ARG001 +def mark_all_drops_fully_imported(apps, schema_editor) -> None: # noqa: ANN001 """Marks all existing DropCampaigns as fully imported. This was needed to ensure that the Twitch API view only returns campaigns that are ready for display. diff --git a/twitch/tests/test_feeds.py b/twitch/tests/test_feeds.py index b69e9cb..afd7a83 100644 --- a/twitch/tests/test_feeds.py +++ b/twitch/tests/test_feeds.py @@ -1410,3 +1410,33 @@ class DiscordFeedTestCase(TestCase): f"Expected Discord timestamp format <t:UNIX_TIMESTAMP:R> in content, got: {content}" ) assert "()" not in content + + def test_feed_links_return_200(self) -> None: + """Test that all links in the feeds return 200 OK.""" + feed_urls: list[str] = [ + reverse("core:organization_feed"), + reverse("core:game_feed"), + reverse("core:organization_feed_atom"), + ] + + for feed_url in feed_urls: + response: _MonkeyPatchedWSGIResponse = self.client.get(feed_url) + assert response.status_code == 200, f"Feed {feed_url} did not return 200." + + # Extract all links from the feed content + content: str = response.content.decode("utf-8") + links: list[str] = re.findall(r'href=["\'](https?://.*?)["\']', content) + + for link in links: + skip_links: list[str] = [ + "rss_styles.xslt", + "twitch.tv", + ] + + if any(skip in link for skip in skip_links): + # Skip testing rss_styles.xslt, external Twitch links, and internal links that may not exist in test environment + continue + link_response: _MonkeyPatchedWSGIResponse = self.client.get(link) + + msg: str = f"Link {link} in feed {feed_url} did not return 200, got {link_response.status_code}." + assert link_response.status_code == 200, msg diff --git a/twitch/views.py b/twitch/views.py index 595bd42..2b492f8 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -1883,7 +1883,7 @@ def export_campaigns_json(request: HttpRequest) -> HttpResponse: return response -def export_games_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG001 # noqa: ARG001 +def export_games_csv(request: HttpRequest) -> HttpResponse: """Export games to CSV format. Args: @@ -1923,7 +1923,7 @@ def export_games_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG001 # n return response -def export_games_json(request: HttpRequest) -> HttpResponse: # noqa: ARG001 # noqa: ARG001 +def export_games_json(request: HttpRequest) -> HttpResponse: """Export games to JSON format. Args: @@ -1958,7 +1958,7 @@ def export_games_json(request: HttpRequest) -> HttpResponse: # noqa: ARG001 # return response -def export_organizations_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG001 +def export_organizations_csv(request: HttpRequest) -> HttpResponse: """Export organizations to CSV format. Args: @@ -1987,7 +1987,7 @@ def export_organizations_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG return response -def export_organizations_json(request: HttpRequest) -> HttpResponse: # noqa: ARG001 +def export_organizations_json(request: HttpRequest) -> HttpResponse: """Export organizations to JSON format. Args: