diff --git a/templates/twitch/badge_list.html b/templates/twitch/badge_list.html index 9c31657..5dfc573 100644 --- a/templates/twitch/badge_list.html +++ b/templates/twitch/badge_list.html @@ -40,7 +40,7 @@ These are the global chat badges available on Twitch. {% endfor %}
{% else %} -

no badge sets found.

+

No badge sets found.

run: uv run python manage.py import_chat_badges

diff --git a/templates/twitch/campaign_detail.html b/templates/twitch/campaign_detail.html index 09afc7f..eeae339 100644 --- a/templates/twitch/campaign_detail.html +++ b/templates/twitch/campaign_detail.html @@ -167,6 +167,9 @@ margin-right: 4px" /> {{ drop.awarded_badge.title }} + {% if drop.awarded_badge.description %} +
{{ drop.awarded_badge.description|linebreaksbr }}
+ {% endif %} {% endif %} {% endfor %} diff --git a/twitch/feeds.py b/twitch/feeds.py index db34669..348f42d 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -16,6 +16,7 @@ from django.utils.safestring import SafeString from django.utils.safestring import SafeText from twitch.models import Channel +from twitch.models import ChatBadge from twitch.models import DropBenefit from twitch.models import DropCampaign from twitch.models import Game @@ -229,6 +230,18 @@ def _construct_drops_summary(drops_data: list[dict]) -> SafeText: if not drops_data: return SafeText("") + badge_titles: set[str] = set() + for drop in drops_data: + for b in drop.get("benefits", []): + if getattr(b, "distribution_type", "") == "BADGE" and getattr(b, "name", ""): + badge_titles.add(b.name) + + badge_descriptions_by_title: dict[str, str] = {} + if badge_titles: + badge_descriptions_by_title = dict( + ChatBadge.objects.filter(title__in=badge_titles).values_list("title", "description"), + ) + def sort_key(drop: dict) -> tuple[bool, int]: req: str = drop.get("requirements", "") m: re.Match[str] | None = re.search(r"(\d+) minutes watched", req) @@ -246,14 +259,19 @@ def _construct_drops_summary(drops_data: list[dict]) -> SafeText: benefit_names: list[tuple[str]] = [] for b in benefits: benefit_name: str = getattr(b, "name", str(b)) + badge_desc: str | None = badge_descriptions_by_title.get(benefit_name) if is_sub_required and channel_name: - benefit_names.append(( - format_html( - '{}', - channel_name, - benefit_name, - ), - )) + linked_name: SafeString = format_html( + '{}', + channel_name, + benefit_name, + ) + if badge_desc: + benefit_names.append((format_html("{} ({})", linked_name, badge_desc),)) + else: + benefit_names.append((linked_name,)) + elif badge_desc: + benefit_names.append((format_html("{} ({})", benefit_name, badge_desc),)) else: benefit_names.append((benefit_name,)) benefits_str: SafeString = format_html_join(", ", "{}", benefit_names) if benefit_names else SafeText("") diff --git a/twitch/tests/test_feeds.py b/twitch/tests/test_feeds.py index 4cf5949..81e9528 100644 --- a/twitch/tests/test_feeds.py +++ b/twitch/tests/test_feeds.py @@ -8,9 +8,13 @@ from django.test import TestCase from django.urls import reverse from django.utils import timezone +from twitch.models import ChatBadge +from twitch.models import ChatBadgeSet +from twitch.models import DropBenefit from twitch.models import DropCampaign from twitch.models import Game from twitch.models import Organization +from twitch.models import TimeBasedDrop class RSSFeedTestCase(TestCase): @@ -59,6 +63,39 @@ class RSSFeedTestCase(TestCase): assert response.status_code == 200 assert response["Content-Type"] == "application/rss+xml; charset=utf-8" + def test_campaign_feed_includes_badge_description(self) -> None: + """Badge benefit descriptions should be visible in the RSS drop summary.""" + drop = TimeBasedDrop.objects.create( + twitch_id="drop-1", + name="Diana Chat Badge", + campaign=self.campaign, + required_minutes_watched=0, + required_subs=1, + ) + benefit = DropBenefit.objects.create( + twitch_id="benefit-1", + name="Diana", + distribution_type="BADGE", + ) + drop.benefits.add(benefit) + + badge_set = ChatBadgeSet.objects.create(set_id="diana") + ChatBadge.objects.create( + badge_set=badge_set, + badge_id="1", + image_url_1x="https://example.com/1x.png", + image_url_2x="https://example.com/2x.png", + image_url_4x="https://example.com/4x.png", + title="Diana", + description="This badge was earned by subscribing.", + ) + + url = reverse("twitch:campaign_feed") + response = self.client.get(url) + assert response.status_code == 200 + content = response.content.decode("utf-8") + assert "This badge was earned by subscribing." in content + def test_game_campaign_feed(self) -> None: """Test game-specific campaign feed returns 200.""" url = reverse("twitch:game_campaign_feed", args=[self.game.twitch_id]) diff --git a/twitch/tests/test_views.py b/twitch/tests/test_views.py index 5ad8110..d59cff1 100644 --- a/twitch/tests/test_views.py +++ b/twitch/tests/test_views.py @@ -10,6 +10,8 @@ from django.urls import reverse from django.utils import timezone from twitch.models import Channel +from twitch.models import ChatBadge +from twitch.models import ChatBadgeSet from twitch.models import DropBenefit from twitch.models import DropCampaign from twitch.models import Game @@ -469,6 +471,54 @@ class TestChannelListView: assert response.status_code == 200 assert "campaign" in response.context + @pytest.mark.django_db + def test_drop_campaign_detail_view_badge_benefit_includes_description_from_chatbadge( + self, + client: Client, + ) -> None: + """Test campaign detail view includes badge benefit description from ChatBadge.""" + game: Game = Game.objects.create(twitch_id="g-badge", name="Game", display_name="Game") + campaign: DropCampaign = DropCampaign.objects.create( + twitch_id="c-badge", + name="Campaign", + game=game, + operation_name="DropCampaignDetails", + ) + + drop = TimeBasedDrop.objects.create( + twitch_id="d1", + name="Drop", + campaign=campaign, + required_minutes_watched=0, + required_subs=1, + ) + + benefit = DropBenefit.objects.create( + twitch_id="b1", + name="Diana", + distribution_type="BADGE", + ) + drop.benefits.add(benefit) + + badge_set = ChatBadgeSet.objects.create(set_id="diana") + ChatBadge.objects.create( + badge_set=badge_set, + badge_id="1", + image_url_1x="https://example.com/1", + image_url_2x="https://example.com/2", + image_url_4x="https://example.com/4", + title="Diana", + description="This badge was earned by subscribing.", + ) + + url: str = reverse("twitch:campaign_detail", args=[campaign.twitch_id]) + response: _MonkeyPatchedWSGIResponse = client.get(url) + assert response.status_code == 200 + + # The campaign detail page prints a syntax-highlighted JSON block; the badge description should be present. + html = response.content.decode("utf-8") + assert "This badge was earned by subscribing." in html + @pytest.mark.django_db def test_games_grid_view(self, client: Client) -> None: """Test games grid view returns 200 and has games in context.""" diff --git a/twitch/views.py b/twitch/views.py index b73b79c..657a279 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -350,7 +350,7 @@ def _enhance_drops_with_context(drops: QuerySet[TimeBasedDrop], now: datetime.da # MARK: /campaigns// -def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpResponse: +def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpResponse: # noqa: PLR0914 """Function-based view for a drop campaign detail. Args: @@ -401,6 +401,16 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo campaign_data = json.loads(serialized_campaign) if drops.exists(): + badge_benefit_names: set[str] = { + benefit.name + for drop in drops + for benefit in drop.benefits.all() + if benefit.distribution_type == "BADGE" and benefit.name + } + badge_descriptions_by_title: dict[str, str] = dict( + ChatBadge.objects.filter(title__in=badge_benefit_names).values_list("title", "description"), + ) + serialized_drops = serialize( "json", drops, @@ -436,6 +446,20 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo ), ) benefits_data = json.loads(serialized_benefits) + + for benefit_data in benefits_data: + fields: dict[str, Any] = benefit_data.get("fields", {}) + if fields.get("distribution_type") != "BADGE": + continue + + # DropBenefit doesn't have a description field; fetch it from ChatBadge when possible. + if fields.get("description"): + continue + + badge_description: str | None = badge_descriptions_by_title.get(fields.get("name", "")) + if badge_description: + fields["description"] = badge_description + drops_data[i]["fields"]["benefits"] = benefits_data campaign_data[0]["fields"]["drops"] = drops_data