Enhance badge descriptions in campaign detail and RSS feed outputs
This commit is contained in:
parent
ada03da440
commit
fad0821515
6 changed files with 141 additions and 9 deletions
|
|
@ -40,7 +40,7 @@ These are the global chat badges available on Twitch.
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<hr />
|
<hr />
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>no badge sets found.</p>
|
<p>No badge sets found.</p>
|
||||||
<p>
|
<p>
|
||||||
run: <code>uv run python manage.py import_chat_badges</code>
|
run: <code>uv run python manage.py import_chat_badges</code>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,9 @@
|
||||||
margin-right: 4px" />
|
margin-right: 4px" />
|
||||||
{{ drop.awarded_badge.title }}
|
{{ drop.awarded_badge.title }}
|
||||||
</a>
|
</a>
|
||||||
|
{% if drop.awarded_badge.description %}
|
||||||
|
<div style="margin-top: 4px; color: #a9a9a9; font-size: 0.9em;">{{ drop.awarded_badge.description|linebreaksbr }}</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from django.utils.safestring import SafeString
|
||||||
from django.utils.safestring import SafeText
|
from django.utils.safestring import SafeText
|
||||||
|
|
||||||
from twitch.models import Channel
|
from twitch.models import Channel
|
||||||
|
from twitch.models import ChatBadge
|
||||||
from twitch.models import DropBenefit
|
from twitch.models import DropBenefit
|
||||||
from twitch.models import DropCampaign
|
from twitch.models import DropCampaign
|
||||||
from twitch.models import Game
|
from twitch.models import Game
|
||||||
|
|
@ -229,6 +230,18 @@ def _construct_drops_summary(drops_data: list[dict]) -> SafeText:
|
||||||
if not drops_data:
|
if not drops_data:
|
||||||
return SafeText("")
|
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]:
|
def sort_key(drop: dict) -> tuple[bool, int]:
|
||||||
req: str = drop.get("requirements", "")
|
req: str = drop.get("requirements", "")
|
||||||
m: re.Match[str] | None = re.search(r"(\d+) minutes watched", req)
|
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]] = []
|
benefit_names: list[tuple[str]] = []
|
||||||
for b in benefits:
|
for b in benefits:
|
||||||
benefit_name: str = getattr(b, "name", str(b))
|
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:
|
if is_sub_required and channel_name:
|
||||||
benefit_names.append((
|
linked_name: SafeString = format_html(
|
||||||
format_html(
|
'<a href="https://twitch.tv/{}" target="_blank">{}</a>',
|
||||||
'<a href="https://twitch.tv/{}" target="_blank">{}</a>',
|
channel_name,
|
||||||
channel_name,
|
benefit_name,
|
||||||
benefit_name,
|
)
|
||||||
),
|
if badge_desc:
|
||||||
))
|
benefit_names.append((format_html("{} (<em>{}</em>)", linked_name, badge_desc),))
|
||||||
|
else:
|
||||||
|
benefit_names.append((linked_name,))
|
||||||
|
elif badge_desc:
|
||||||
|
benefit_names.append((format_html("{} (<em>{}</em>)", benefit_name, badge_desc),))
|
||||||
else:
|
else:
|
||||||
benefit_names.append((benefit_name,))
|
benefit_names.append((benefit_name,))
|
||||||
benefits_str: SafeString = format_html_join(", ", "{}", benefit_names) if benefit_names else SafeText("")
|
benefits_str: SafeString = format_html_join(", ", "{}", benefit_names) if benefit_names else SafeText("")
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,13 @@ from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
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 DropCampaign
|
||||||
from twitch.models import Game
|
from twitch.models import Game
|
||||||
from twitch.models import Organization
|
from twitch.models import Organization
|
||||||
|
from twitch.models import TimeBasedDrop
|
||||||
|
|
||||||
|
|
||||||
class RSSFeedTestCase(TestCase):
|
class RSSFeedTestCase(TestCase):
|
||||||
|
|
@ -59,6 +63,39 @@ class RSSFeedTestCase(TestCase):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
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:
|
def test_game_campaign_feed(self) -> None:
|
||||||
"""Test game-specific campaign feed returns 200."""
|
"""Test game-specific campaign feed returns 200."""
|
||||||
url = reverse("twitch:game_campaign_feed", args=[self.game.twitch_id])
|
url = reverse("twitch:game_campaign_feed", args=[self.game.twitch_id])
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from twitch.models import Channel
|
from twitch.models import Channel
|
||||||
|
from twitch.models import ChatBadge
|
||||||
|
from twitch.models import ChatBadgeSet
|
||||||
from twitch.models import DropBenefit
|
from twitch.models import DropBenefit
|
||||||
from twitch.models import DropCampaign
|
from twitch.models import DropCampaign
|
||||||
from twitch.models import Game
|
from twitch.models import Game
|
||||||
|
|
@ -469,6 +471,54 @@ class TestChannelListView:
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "campaign" in response.context
|
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
|
@pytest.mark.django_db
|
||||||
def test_games_grid_view(self, client: Client) -> None:
|
def test_games_grid_view(self, client: Client) -> None:
|
||||||
"""Test games grid view returns 200 and has games in context."""
|
"""Test games grid view returns 200 and has games in context."""
|
||||||
|
|
|
||||||
|
|
@ -350,7 +350,7 @@ def _enhance_drops_with_context(drops: QuerySet[TimeBasedDrop], now: datetime.da
|
||||||
|
|
||||||
|
|
||||||
# MARK: /campaigns/<twitch_id>/
|
# MARK: /campaigns/<twitch_id>/
|
||||||
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.
|
"""Function-based view for a drop campaign detail.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -401,6 +401,16 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
||||||
campaign_data = json.loads(serialized_campaign)
|
campaign_data = json.loads(serialized_campaign)
|
||||||
|
|
||||||
if drops.exists():
|
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(
|
serialized_drops = serialize(
|
||||||
"json",
|
"json",
|
||||||
drops,
|
drops,
|
||||||
|
|
@ -436,6 +446,20 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
benefits_data = json.loads(serialized_benefits)
|
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
|
drops_data[i]["fields"]["benefits"] = benefits_data
|
||||||
|
|
||||||
campaign_data[0]["fields"]["drops"] = drops_data
|
campaign_data[0]["fields"]["drops"] = drops_data
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue