Use the reward image instead of campaign if only one reward
All checks were successful
Deploy to Server / deploy (push) Successful in 26s
All checks were successful
Deploy to Server / deploy (push) Successful in 26s
This commit is contained in:
parent
bc075bc95e
commit
b06dd6b1ac
3 changed files with 187 additions and 9 deletions
|
|
@ -580,7 +580,7 @@ class Channel(auto_prefetch.Model):
|
|||
|
||||
|
||||
# MARK: DropCampaign
|
||||
class DropCampaign(auto_prefetch.Model):
|
||||
class DropCampaign(auto_prefetch.Model): # noqa: PLR0904
|
||||
"""Represents a Twitch drop campaign."""
|
||||
|
||||
twitch_id = models.TextField(
|
||||
|
|
@ -1148,6 +1148,21 @@ class DropCampaign(auto_prefetch.Model):
|
|||
).order_by("display_name"),
|
||||
to_attr="channels_ordered",
|
||||
),
|
||||
Prefetch(
|
||||
"time_based_drops",
|
||||
queryset=TimeBasedDrop.objects.only(
|
||||
"twitch_id",
|
||||
"campaign_id",
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
"benefits",
|
||||
queryset=DropBenefit.objects.only(
|
||||
"twitch_id",
|
||||
"image_asset_url",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.order_by("-start_at")
|
||||
)
|
||||
|
|
@ -1268,6 +1283,38 @@ class DropCampaign(auto_prefetch.Model):
|
|||
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def single_reward_benefit(self) -> DropBenefit | None:
|
||||
"""Return the only unique reward benefit for this campaign, if it has one."""
|
||||
benefits: list[DropBenefit] = []
|
||||
seen_benefit_keys: set[int | str] = set()
|
||||
|
||||
for drop in self.time_based_drops.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||
for benefit in drop.benefits.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||
benefit_key: int | str = benefit.pk or benefit.twitch_id
|
||||
if benefit_key in seen_benefit_keys:
|
||||
continue
|
||||
|
||||
seen_benefit_keys.add(benefit_key)
|
||||
benefits.append(benefit)
|
||||
if len(benefits) > 1:
|
||||
return None
|
||||
|
||||
return benefits[0] if benefits else None
|
||||
|
||||
@property
|
||||
def single_reward_image_best_url(self) -> str:
|
||||
"""Return the best image URL for a campaign that has exactly one reward."""
|
||||
benefit: DropBenefit | None = self.single_reward_benefit
|
||||
if not benefit:
|
||||
return ""
|
||||
return benefit.image_best_url
|
||||
|
||||
@property
|
||||
def meta_image_url(self) -> str:
|
||||
"""Return the preferred campaign image URL for SEO metadata."""
|
||||
return self.single_reward_image_best_url or self.image_best_url
|
||||
|
||||
@property
|
||||
def image_best_url(self) -> str:
|
||||
"""Return the best URL for the campaign image.
|
||||
|
|
@ -1311,7 +1358,10 @@ class DropCampaign(auto_prefetch.Model):
|
|||
|
||||
@property
|
||||
def dashboard_image_url(self) -> str:
|
||||
"""Return dashboard-safe campaign image URL without touching deferred image fields."""
|
||||
"""Return dashboard-safe campaign or single-reward image URL."""
|
||||
benefit: DropBenefit | None = self.single_reward_benefit
|
||||
if benefit and benefit.image_asset_url:
|
||||
return benefit.image_asset_url
|
||||
return self.image_url or ""
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -1057,6 +1057,46 @@ class TestChannelListView:
|
|||
|
||||
assert len(capture) == 0
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dashboard_uses_single_reward_image_for_campaign_card(self) -> None:
|
||||
"""Dashboard campaign cards should show the reward image for one-reward campaigns."""
|
||||
now: datetime.datetime = timezone.now()
|
||||
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="game_dashboard_single_reward_image",
|
||||
name="game_dashboard_single_reward_image",
|
||||
display_name="Game Dashboard Single Reward Image",
|
||||
)
|
||||
campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="campaign_dashboard_single_reward_image",
|
||||
name="Campaign Dashboard Single Reward Image",
|
||||
game=game,
|
||||
operation_names=["DropCampaignDetails"],
|
||||
image_url="https://example.com/campaign.png",
|
||||
start_at=now - timedelta(hours=1),
|
||||
end_at=now + timedelta(hours=1),
|
||||
)
|
||||
benefit: DropBenefit = DropBenefit.objects.create(
|
||||
twitch_id="benefit_dashboard_single_reward_image",
|
||||
name="Benefit Dashboard Single Reward Image",
|
||||
image_asset_url="https://example.com/benefit.png",
|
||||
)
|
||||
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
|
||||
twitch_id="drop_dashboard_single_reward_image",
|
||||
name="Drop Dashboard Single Reward Image",
|
||||
campaign=campaign,
|
||||
)
|
||||
drop.benefits.add(benefit)
|
||||
|
||||
campaigns_by_game: OrderedDict[str, dict[str, Any]] = (
|
||||
DropCampaign.campaigns_by_game_for_dashboard(now)
|
||||
)
|
||||
|
||||
assert (
|
||||
campaigns_by_game[game.twitch_id]["campaigns"][0]["image_url"]
|
||||
== "https://example.com/benefit.png"
|
||||
)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dashboard_query_plans_reference_expected_index_names(self) -> None:
|
||||
"""Dashboard active-window plans should mention concrete index names."""
|
||||
|
|
@ -2939,6 +2979,31 @@ class TestSEOMetaTags:
|
|||
assert "modified_date" in response.context
|
||||
assert response.context["modified_date"] is not None
|
||||
|
||||
def test_campaign_detail_meta_image_uses_single_reward_image(
|
||||
self,
|
||||
client: Client,
|
||||
game_with_campaign: dict[str, Any],
|
||||
) -> None:
|
||||
"""Campaign detail SEO image should prefer the reward image for one-reward campaigns."""
|
||||
campaign: DropCampaign = game_with_campaign["campaign"]
|
||||
benefit: DropBenefit = DropBenefit.objects.create(
|
||||
twitch_id="seo-single-reward-benefit",
|
||||
name="SEO Single Reward Benefit",
|
||||
image_asset_url="https://example.com/seo-benefit.png",
|
||||
)
|
||||
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
|
||||
twitch_id="seo-single-reward-drop",
|
||||
name="SEO Single Reward Drop",
|
||||
campaign=campaign,
|
||||
)
|
||||
drop.benefits.add(benefit)
|
||||
|
||||
url = reverse("twitch:campaign_detail", args=[campaign.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.context["page_image"] == "https://example.com/seo-benefit.png"
|
||||
|
||||
def test_game_detail_view_has_seo_context(
|
||||
self,
|
||||
client: Client,
|
||||
|
|
@ -3506,6 +3571,65 @@ class TestDropCampaignImageFallback:
|
|||
# Should return campaign image, not benefit image
|
||||
assert campaign.image_best_url == "https://example.com/campaign.png"
|
||||
|
||||
def test_meta_image_url_prefers_single_reward_image(self) -> None:
|
||||
"""Meta image should use the reward image when a campaign has one reward."""
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="game1",
|
||||
name="test_game",
|
||||
display_name="Test Game",
|
||||
)
|
||||
campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="camp1",
|
||||
name="Test Campaign",
|
||||
game=game,
|
||||
image_url="https://example.com/campaign.png",
|
||||
)
|
||||
benefit: DropBenefit = DropBenefit.objects.create(
|
||||
twitch_id="benefit1",
|
||||
name="Test Benefit",
|
||||
image_asset_url="https://example.com/benefit.png",
|
||||
)
|
||||
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
|
||||
twitch_id="drop1",
|
||||
name="Test Drop",
|
||||
campaign=campaign,
|
||||
)
|
||||
drop.benefits.add(benefit)
|
||||
|
||||
assert campaign.meta_image_url == "https://example.com/benefit.png"
|
||||
|
||||
def test_meta_image_url_uses_campaign_image_with_multiple_rewards(self) -> None:
|
||||
"""Meta image should keep the campaign image when a campaign has many rewards."""
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="game1",
|
||||
name="test_game",
|
||||
display_name="Test Game",
|
||||
)
|
||||
campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="camp1",
|
||||
name="Test Campaign",
|
||||
game=game,
|
||||
image_url="https://example.com/campaign.png",
|
||||
)
|
||||
first_benefit: DropBenefit = DropBenefit.objects.create(
|
||||
twitch_id="benefit1",
|
||||
name="Test Benefit 1",
|
||||
image_asset_url="https://example.com/benefit-1.png",
|
||||
)
|
||||
second_benefit: DropBenefit = DropBenefit.objects.create(
|
||||
twitch_id="benefit2",
|
||||
name="Test Benefit 2",
|
||||
image_asset_url="https://example.com/benefit-2.png",
|
||||
)
|
||||
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
|
||||
twitch_id="drop1",
|
||||
name="Test Drop",
|
||||
campaign=campaign,
|
||||
)
|
||||
drop.benefits.add(first_benefit, second_benefit)
|
||||
|
||||
assert campaign.meta_image_url == "https://example.com/campaign.png"
|
||||
|
||||
def test_image_best_url_returns_empty_when_no_images(self) -> None:
|
||||
"""Test that image_best_url returns empty string when no images available."""
|
||||
game: Game = Game.objects.create(
|
||||
|
|
|
|||
|
|
@ -520,13 +520,17 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
|||
if campaign.description
|
||||
else f"Twitch drop campaign: {campaign_name}"
|
||||
)
|
||||
campaign_image: str | None = campaign.image_best_url
|
||||
campaign_image_width: int | None = (
|
||||
campaign.image_width if campaign.image_file else None
|
||||
)
|
||||
campaign_image_height: int | None = (
|
||||
campaign.image_height if campaign.image_file else None
|
||||
)
|
||||
single_reward: DropBenefit | None = campaign.single_reward_benefit
|
||||
single_reward_image: str = single_reward.image_best_url if single_reward else ""
|
||||
campaign_image: str | None = single_reward_image or campaign.image_best_url
|
||||
campaign_image_width: int | None = None
|
||||
campaign_image_height: int | None = None
|
||||
if single_reward_image and single_reward and single_reward.image_file:
|
||||
campaign_image_width = single_reward.image_width
|
||||
campaign_image_height = single_reward.image_height
|
||||
elif campaign.image_file:
|
||||
campaign_image_width = campaign.image_width
|
||||
campaign_image_height = campaign.image_height
|
||||
|
||||
url: str = build_absolute_uri(
|
||||
reverse("twitch:campaign_detail", args=[campaign.twitch_id]),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue