Compare commits

...

3 commits

Author SHA1 Message Date
ef00b4c020
Fix feed links
All checks were successful
Deploy to Server / deploy (push) Successful in 19s
2026-03-22 01:12:23 +01:00
867ddf7676
Update game detail template to improve box art alt text 2026-03-22 00:42:55 +01:00
8dfcd118e5
Fix pyright attribute access issues in test_sitemaps and fix import in views 2026-03-22 00:34:01 +01:00
9 changed files with 52 additions and 22 deletions

View file

@ -23,7 +23,7 @@ def test_sitemap_static_contains_expected_links(
youtube apps as well as some misc static files like /robots.txt. youtube apps as well as some misc static files like /robots.txt.
""" """
# Ensure deterministic BASE_URL # 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")) response = client.get(reverse("sitemap-static"))
assert response.status_code == 200 assert response.status_code == 200
@ -31,7 +31,7 @@ def test_sitemap_static_contains_expected_links(
locs = _extract_locs(response.content) locs = _extract_locs(response.content)
base = settings.BASE_URL.rstrip("/") base = settings.BASE_URL.rstrip("/") # pyright: ignore[reportAttributeAccessIssue]
expected_paths = [ expected_paths = [
reverse("core:dashboard"), reverse("core:dashboard"),

View file

@ -22,8 +22,8 @@ from django.http import Http404
from django.http import HttpRequest from django.http import HttpRequest
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import render from django.shortcuts import render
from django.shortcuts import reverse
from django.template.defaultfilters import filesizeformat from django.template.defaultfilters import filesizeformat
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from kick.models import KickChannel from kick.models import KickChannel

View file

@ -5,14 +5,20 @@ description = "Get notified when a new drop is available on Twitch."
readme = "README.md" readme = "README.md"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [
"celery[redis]",
"colorama", "colorama",
"dateparser", "dateparser",
"django-auto-prefetch", "django-auto-prefetch",
"django-celery-beat",
"django-celery-results",
"django-debug-toolbar", "django-debug-toolbar",
"django-silk", "django-silk",
"django", "django",
"flower",
"gunicorn", "gunicorn",
"hiredis",
"httpx", "httpx",
"index-now-for-python",
"json-repair", "json-repair",
"pillow", "pillow",
"platformdirs", "platformdirs",
@ -20,17 +26,11 @@ dependencies = [
"pydantic", "pydantic",
"pygments", "pygments",
"python-dotenv", "python-dotenv",
"redis",
"sentry-sdk", "sentry-sdk",
"setproctitle", "setproctitle",
"tqdm",
"index-now-for-python",
"sitemap-parser", "sitemap-parser",
"celery[redis]", "tqdm",
"django-celery-results",
"django-celery-beat",
"flower",
"hiredis",
"redis",
] ]
@ -41,9 +41,9 @@ dev = [
"hypothesis[django]", "hypothesis[django]",
"pytest-cov", "pytest-cov",
"pytest-django", "pytest-django",
"pytest-randomly",
"pytest-xdist[psutil]", "pytest-xdist[psutil]",
"pytest", "pytest",
"pytest-randomly",
] ]
[tool.pytest.ini_options] [tool.pytest.ini_options]

View file

@ -24,7 +24,7 @@
<!-- Game image --> <!-- Game image -->
<div style="margin-right: 16px;"> <div style="margin-right: 16px;">
{% if game.box_art_best_url %} {% 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 %} {% endif %}
</div> </div>
<!-- Game Title and Details --> <!-- Game Title and Details -->

View file

@ -692,7 +692,7 @@ class OrganizationRSSFeed(TTVDropsBaseFeed):
feed_type = BrowserFriendlyRss201rev2Feed feed_type = BrowserFriendlyRss201rev2Feed
title: str = "TTVDrops Twitch Organizations" title: str = "TTVDrops Twitch Organizations"
link: str = "/organizations/" link: str = "/twitch/organizations/"
description: str = "Latest organizations on TTVDrops" description: str = "Latest organizations on TTVDrops"
_limit: int | None = None _limit: int | None = None
@ -921,7 +921,7 @@ class DropCampaignFeed(TTVDropsBaseFeed):
"""RSS feed for latest drop campaigns.""" """RSS feed for latest drop campaigns."""
title: str = "Twitch Drop Campaigns" title: str = "Twitch Drop Campaigns"
link: str = "/campaigns/" link: str = "/"
description: str = "Latest Twitch drop campaigns on TTVDrops" description: str = "Latest Twitch drop campaigns on TTVDrops"
item_guid_is_permalink = True item_guid_is_permalink = True

View file

@ -5,7 +5,7 @@ from django.db import migrations
from django.db import models 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.""" """Convert operation_name string values to operation_names list."""
DropCampaign = apps.get_model("twitch", "DropCampaign") DropCampaign = apps.get_model("twitch", "DropCampaign")
for campaign in DropCampaign.objects.all(): 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"]) 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).""" """Convert operation_names list back to operation_name string (first item only)."""
DropCampaign = apps.get_model("twitch", "DropCampaign") DropCampaign = apps.get_model("twitch", "DropCampaign")
for campaign in DropCampaign.objects.all(): for campaign in DropCampaign.objects.all():

View file

@ -1,7 +1,7 @@
from django.db import migrations 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. """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. This was needed to ensure that the Twitch API view only returns campaigns that are ready for display.

View file

@ -1410,3 +1410,33 @@ class DiscordFeedTestCase(TestCase):
f"Expected Discord timestamp format &amp;lt;t:UNIX_TIMESTAMP:R&amp;gt; in content, got: {content}" f"Expected Discord timestamp format &amp;lt;t:UNIX_TIMESTAMP:R&amp;gt; in content, got: {content}"
) )
assert "()" not in 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

View file

@ -1883,7 +1883,7 @@ def export_campaigns_json(request: HttpRequest) -> HttpResponse:
return response 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. """Export games to CSV format.
Args: Args:
@ -1923,7 +1923,7 @@ def export_games_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG001 # n
return response 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. """Export games to JSON format.
Args: Args:
@ -1958,7 +1958,7 @@ def export_games_json(request: HttpRequest) -> HttpResponse: # noqa: ARG001 #
return response return response
def export_organizations_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG001 def export_organizations_csv(request: HttpRequest) -> HttpResponse:
"""Export organizations to CSV format. """Export organizations to CSV format.
Args: Args:
@ -1987,7 +1987,7 @@ def export_organizations_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG
return response return response
def export_organizations_json(request: HttpRequest) -> HttpResponse: # noqa: ARG001 def export_organizations_json(request: HttpRequest) -> HttpResponse:
"""Export organizations to JSON format. """Export organizations to JSON format.
Args: Args: