Add DropCampaign image fallback logic and update templates for best image URL
This commit is contained in:
parent
55c2273e27
commit
7f468bbabe
8 changed files with 438 additions and 17 deletions
265
example.json
Normal file
265
example.json
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"id": "17658559",
|
||||||
|
"dropCampaign": {
|
||||||
|
"id": "3b965979-ecd2-11f0-876e-0a58a9feac02",
|
||||||
|
"self": {
|
||||||
|
"isAccountConnected": true,
|
||||||
|
"__typename": "DropCampaignSelfEdge"
|
||||||
|
},
|
||||||
|
"allow": {
|
||||||
|
"channels": null,
|
||||||
|
"isEnabled": false,
|
||||||
|
"__typename": "DropCampaignACL"
|
||||||
|
},
|
||||||
|
"accountLinkURL": "https://link.smite2.com/",
|
||||||
|
"description": "Viewers will receive 50 Wandering Market Coins for each two hours spent viewing participating streams. Watch to earn 7 drops for a total of 350 Wandering Market Coins for the week!",
|
||||||
|
"detailsURL": "https://www.smite2.com/news/closed-alpha-twitch-drops/",
|
||||||
|
"endAt": "2026-01-17T10:58:59.999Z",
|
||||||
|
"eventBasedDrops": [],
|
||||||
|
"game": {
|
||||||
|
"id": "2094865572",
|
||||||
|
"slug": "smite-2",
|
||||||
|
"displayName": "SMITE 2",
|
||||||
|
"__typename": "Game"
|
||||||
|
},
|
||||||
|
"imageURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/47db66e8-933c-484f-ab5a-30ba09093098.png",
|
||||||
|
"name": "Jan Drops Week 2",
|
||||||
|
"owner": {
|
||||||
|
"id": "51a157a0-674a-4863-b120-7bb6ee2466a8",
|
||||||
|
"name": "Hi-Rez Studios",
|
||||||
|
"__typename": "Organization"
|
||||||
|
},
|
||||||
|
"startAt": "2026-01-10T11:00:00Z",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"timeBasedDrops": [
|
||||||
|
{
|
||||||
|
"id": "933c8f91-ecd2-11f0-b3fd-0a58a9feac02",
|
||||||
|
"requiredSubs": 0,
|
||||||
|
"benefitEdges": [
|
||||||
|
{
|
||||||
|
"benefit": {
|
||||||
|
"id": "ccb3fb7f-e59b-11ef-aef0-0a58a9feac02",
|
||||||
|
"createdAt": "2025-02-07T21:37:58.881Z",
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"game": {
|
||||||
|
"id": "2094865572",
|
||||||
|
"name": "SMITE 2",
|
||||||
|
"__typename": "Game"
|
||||||
|
},
|
||||||
|
"imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/903496ad-de97-41ff-ad97-12f099e20ea8.jpeg",
|
||||||
|
"isIosAvailable": false,
|
||||||
|
"name": "Market Coins Bundle 1",
|
||||||
|
"ownerOrganization": {
|
||||||
|
"id": "51a157a0-674a-4863-b120-7bb6ee2466a8",
|
||||||
|
"name": "Hi-Rez Studios",
|
||||||
|
"__typename": "Organization"
|
||||||
|
},
|
||||||
|
"distributionType": "DIRECT_ENTITLEMENT",
|
||||||
|
"__typename": "DropBenefit"
|
||||||
|
},
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"__typename": "DropBenefitEdge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endAt": "2026-01-17T10:58:59.999Z",
|
||||||
|
"name": "Market Coins Bundle 1",
|
||||||
|
"preconditionDrops": null,
|
||||||
|
"requiredMinutesWatched": 120,
|
||||||
|
"startAt": "2026-01-10T11:00:00Z",
|
||||||
|
"__typename": "TimeBasedDrop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9909373d-ecd2-11f0-92b1-0a58a9feac02",
|
||||||
|
"requiredSubs": 0,
|
||||||
|
"benefitEdges": [
|
||||||
|
{
|
||||||
|
"benefit": {
|
||||||
|
"id": "ccb3fb7f-e59b-11ef-aef0-0a58a9feac02",
|
||||||
|
"createdAt": "2025-02-07T21:37:58.881Z",
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"game": {
|
||||||
|
"id": "2094865572",
|
||||||
|
"name": "SMITE 2",
|
||||||
|
"__typename": "Game"
|
||||||
|
},
|
||||||
|
"imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/903496ad-de97-41ff-ad97-12f099e20ea8.jpeg",
|
||||||
|
"isIosAvailable": false,
|
||||||
|
"name": "Market Coins Bundle 1",
|
||||||
|
"ownerOrganization": {
|
||||||
|
"id": "51a157a0-674a-4863-b120-7bb6ee2466a8",
|
||||||
|
"name": "Hi-Rez Studios",
|
||||||
|
"__typename": "Organization"
|
||||||
|
},
|
||||||
|
"distributionType": "DIRECT_ENTITLEMENT",
|
||||||
|
"__typename": "DropBenefit"
|
||||||
|
},
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"__typename": "DropBenefitEdge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endAt": "2026-01-17T10:58:59.999Z",
|
||||||
|
"name": "Market Coins Bundle 2",
|
||||||
|
"preconditionDrops": null,
|
||||||
|
"requiredMinutesWatched": 240,
|
||||||
|
"startAt": "2026-01-10T11:00:00Z",
|
||||||
|
"__typename": "TimeBasedDrop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a5289489-ecd2-11f0-b098-0a58a9feac02",
|
||||||
|
"requiredSubs": 0,
|
||||||
|
"benefitEdges": [
|
||||||
|
{
|
||||||
|
"benefit": {
|
||||||
|
"id": "ccb3fb7f-e59b-11ef-aef0-0a58a9feac02",
|
||||||
|
"createdAt": "2025-02-07T21:37:58.881Z",
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"game": {
|
||||||
|
"id": "2094865572",
|
||||||
|
"name": "SMITE 2",
|
||||||
|
"__typename": "Game"
|
||||||
|
},
|
||||||
|
"imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/903496ad-de97-41ff-ad97-12f099e20ea8.jpeg",
|
||||||
|
"isIosAvailable": false,
|
||||||
|
"name": "Market Coins Bundle 1",
|
||||||
|
"ownerOrganization": {
|
||||||
|
"id": "51a157a0-674a-4863-b120-7bb6ee2466a8",
|
||||||
|
"name": "Hi-Rez Studios",
|
||||||
|
"__typename": "Organization"
|
||||||
|
},
|
||||||
|
"distributionType": "DIRECT_ENTITLEMENT",
|
||||||
|
"__typename": "DropBenefit"
|
||||||
|
},
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"__typename": "DropBenefitEdge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endAt": "2026-01-17T10:58:59.999Z",
|
||||||
|
"name": "Market Coins Bundle 3",
|
||||||
|
"preconditionDrops": null,
|
||||||
|
"requiredMinutesWatched": 360,
|
||||||
|
"startAt": "2026-01-10T11:00:00Z",
|
||||||
|
"__typename": "TimeBasedDrop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ab5ea171-ecd2-11f0-9e33-0a58a9feac02",
|
||||||
|
"requiredSubs": 0,
|
||||||
|
"benefitEdges": [
|
||||||
|
{
|
||||||
|
"benefit": {
|
||||||
|
"id": "ccb3fb7f-e59b-11ef-aef0-0a58a9feac02",
|
||||||
|
"createdAt": "2025-02-07T21:37:58.881Z",
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"game": {
|
||||||
|
"id": "2094865572",
|
||||||
|
"name": "SMITE 2",
|
||||||
|
"__typename": "Game"
|
||||||
|
},
|
||||||
|
"imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/903496ad-de97-41ff-ad97-12f099e20ea8.jpeg",
|
||||||
|
"isIosAvailable": false,
|
||||||
|
"name": "Market Coins Bundle 1",
|
||||||
|
"ownerOrganization": {
|
||||||
|
"id": "51a157a0-674a-4863-b120-7bb6ee2466a8",
|
||||||
|
"name": "Hi-Rez Studios",
|
||||||
|
"__typename": "Organization"
|
||||||
|
},
|
||||||
|
"distributionType": "DIRECT_ENTITLEMENT",
|
||||||
|
"__typename": "DropBenefit"
|
||||||
|
},
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"__typename": "DropBenefitEdge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endAt": "2026-01-17T10:58:59.999Z",
|
||||||
|
"name": "Market Coins Bundle 4",
|
||||||
|
"preconditionDrops": null,
|
||||||
|
"requiredMinutesWatched": 480,
|
||||||
|
"startAt": "2026-01-10T11:00:00Z",
|
||||||
|
"__typename": "TimeBasedDrop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b19b7afb-ecd2-11f0-bbd3-0a58a9feac02",
|
||||||
|
"requiredSubs": 0,
|
||||||
|
"benefitEdges": [
|
||||||
|
{
|
||||||
|
"benefit": {
|
||||||
|
"id": "ccb3fb7f-e59b-11ef-aef0-0a58a9feac02",
|
||||||
|
"createdAt": "2025-02-07T21:37:58.881Z",
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"game": {
|
||||||
|
"id": "2094865572",
|
||||||
|
"name": "SMITE 2",
|
||||||
|
"__typename": "Game"
|
||||||
|
},
|
||||||
|
"imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/903496ad-de97-41ff-ad97-12f099e20ea8.jpeg",
|
||||||
|
"isIosAvailable": false,
|
||||||
|
"name": "Market Coins Bundle 1",
|
||||||
|
"ownerOrganization": {
|
||||||
|
"id": "51a157a0-674a-4863-b120-7bb6ee2466a8",
|
||||||
|
"name": "Hi-Rez Studios",
|
||||||
|
"__typename": "Organization"
|
||||||
|
},
|
||||||
|
"distributionType": "DIRECT_ENTITLEMENT",
|
||||||
|
"__typename": "DropBenefit"
|
||||||
|
},
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"__typename": "DropBenefitEdge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endAt": "2026-01-17T10:58:59.999Z",
|
||||||
|
"name": "Market Coins Bundle 5",
|
||||||
|
"preconditionDrops": null,
|
||||||
|
"requiredMinutesWatched": 600,
|
||||||
|
"startAt": "2026-01-10T11:00:00Z",
|
||||||
|
"__typename": "TimeBasedDrop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b82db8e0-ecd2-11f0-8c96-0a58a9feac02",
|
||||||
|
"requiredSubs": 0,
|
||||||
|
"benefitEdges": [
|
||||||
|
{
|
||||||
|
"benefit": {
|
||||||
|
"id": "ccb3fb7f-e59b-11ef-aef0-0a58a9feac02",
|
||||||
|
"createdAt": "2025-02-07T21:37:58.881Z",
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"game": {
|
||||||
|
"id": "2094865572",
|
||||||
|
"name": "SMITE 2",
|
||||||
|
"__typename": "Game"
|
||||||
|
},
|
||||||
|
"imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/903496ad-de97-41ff-ad97-12f099e20ea8.jpeg",
|
||||||
|
"isIosAvailable": false,
|
||||||
|
"name": "Market Coins Bundle 1",
|
||||||
|
"ownerOrganization": {
|
||||||
|
"id": "51a157a0-674a-4863-b120-7bb6ee2466a8",
|
||||||
|
"name": "Hi-Rez Studios",
|
||||||
|
"__typename": "Organization"
|
||||||
|
},
|
||||||
|
"distributionType": "DIRECT_ENTITLEMENT",
|
||||||
|
"__typename": "DropBenefit"
|
||||||
|
},
|
||||||
|
"entitlementLimit": 1,
|
||||||
|
"__typename": "DropBenefitEdge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endAt": "2026-01-17T10:58:59.999Z",
|
||||||
|
"name": "Market Coins Bundle 6",
|
||||||
|
"preconditionDrops": null,
|
||||||
|
"requiredMinutesWatched": 720,
|
||||||
|
"startAt": "2026-01-10T11:00:00Z",
|
||||||
|
"__typename": "TimeBasedDrop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"__typename": "DropCampaign"
|
||||||
|
},
|
||||||
|
"__typename": "User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"durationMilliseconds": 48,
|
||||||
|
"operationName": "DropCampaignDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -19,10 +19,10 @@
|
||||||
</p>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<!-- Campaign image -->
|
<!-- Campaign image -->
|
||||||
{% if campaign.image_url %}
|
{% if campaign.image_best_url %}
|
||||||
<img height="160"
|
<img height="160"
|
||||||
width="160"
|
width="160"
|
||||||
src="{{ campaign.image_best_url|default:campaign.image_url }}"
|
src="{{ campaign.image_best_url }}"
|
||||||
alt="{{ campaign.name }}" />
|
alt="{{ campaign.name }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Campaign description -->
|
<!-- Campaign description -->
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,8 @@
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'twitch:campaign_detail' campaign.twitch_id %}"
|
<a href="{% url 'twitch:campaign_detail' campaign.twitch_id %}"
|
||||||
style="text-decoration: none">
|
style="text-decoration: none">
|
||||||
{% if campaign.image_best_url or campaign.image_url %}
|
{% if campaign.image_best_url %}
|
||||||
<img src="{{ campaign.image_best_url|default:campaign.image_url }}"
|
<img src="{{ campaign.image_best_url }}"
|
||||||
alt="Campaign artwork for {{ campaign.name }}"
|
alt="Campaign artwork for {{ campaign.name }}"
|
||||||
width="120"
|
width="120"
|
||||||
height="120"
|
height="120"
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>Campaigns without Image URLs ({{ broken_image_campaigns|length }})</h2>
|
<h2>Campaigns without Images ({{ broken_image_campaigns|length }})</h2>
|
||||||
{% if broken_image_campaigns %}
|
{% if broken_image_campaigns %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for c in broken_image_campaigns %}
|
{% for c in broken_image_campaigns %}
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>Active Campaigns Missing Image ({{ active_missing_image|length }})</h2>
|
<h2>Active Campaigns without Images ({{ active_missing_image|length }})</h2>
|
||||||
{% if active_missing_image %}
|
{% if active_missing_image %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for c in active_missing_image %}
|
{% for c in active_missing_image %}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@
|
||||||
<a href="{% url 'twitch:reward_campaign_list' %}">← Back to Reward Campaigns</a>
|
<a href="{% url 'twitch:reward_campaign_list' %}">← Back to Reward Campaigns</a>
|
||||||
</p>
|
</p>
|
||||||
<!-- Campaign image -->
|
<!-- Campaign image -->
|
||||||
{% if reward_campaign.image_url %}
|
{% if reward_campaign.image_best_url %}
|
||||||
<img height="160"
|
<img height="160"
|
||||||
width="160"
|
width="160"
|
||||||
src="{{ reward_campaign.image_best_url|default:reward_campaign.image_url }}"
|
src="{{ reward_campaign.image_best_url }}"
|
||||||
alt="{{ reward_campaign.name }}" />
|
alt="{{ reward_campaign.name }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- RSS Feeds -->
|
<!-- RSS Feeds -->
|
||||||
|
|
|
||||||
|
|
@ -446,7 +446,13 @@ class DropCampaign(auto_prefetch.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image_best_url(self) -> str:
|
def image_best_url(self) -> str:
|
||||||
"""Return the best URL for the campaign image (local first)."""
|
"""Return the best URL for the campaign image.
|
||||||
|
|
||||||
|
Priority:
|
||||||
|
1. Local cached image file
|
||||||
|
2. Campaign image URL
|
||||||
|
3. First benefit image URL (if campaign has no image)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
if self.image_file and getattr(self.image_file, "url", None):
|
if self.image_file and getattr(self.image_file, "url", None):
|
||||||
return self.image_file.url
|
return self.image_file.url
|
||||||
|
|
@ -455,7 +461,18 @@ class DropCampaign(auto_prefetch.Model):
|
||||||
"Failed to resolve DropCampaign.image_file url: %s",
|
"Failed to resolve DropCampaign.image_file url: %s",
|
||||||
exc,
|
exc,
|
||||||
)
|
)
|
||||||
return self.image_url or ""
|
|
||||||
|
if self.image_url:
|
||||||
|
return self.image_url
|
||||||
|
|
||||||
|
# If no campaign image, use the first benefit image
|
||||||
|
for drop in self.time_based_drops.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
for benefit in drop.benefits.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
benefit_image_url: str = benefit.image_best_url
|
||||||
|
if benefit_image_url:
|
||||||
|
return benefit_image_url
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duration_iso(self) -> str:
|
def duration_iso(self) -> str:
|
||||||
|
|
@ -537,7 +554,7 @@ class DropCampaign(auto_prefetch.Model):
|
||||||
|
|
||||||
def get_feed_enclosure_url(self) -> str:
|
def get_feed_enclosure_url(self) -> str:
|
||||||
"""Return the campaign image URL for RSS enclosures."""
|
"""Return the campaign image URL for RSS enclosures."""
|
||||||
return self.image_url
|
return self.image_best_url
|
||||||
|
|
||||||
|
|
||||||
# MARK: DropBenefit
|
# MARK: DropBenefit
|
||||||
|
|
|
||||||
|
|
@ -1322,3 +1322,123 @@ class TestSEOPaginationLinks:
|
||||||
# Should be a dict with rel and url
|
# Should be a dict with rel and url
|
||||||
assert isinstance(pagination_info, dict)
|
assert isinstance(pagination_info, dict)
|
||||||
assert "rel" in pagination_info or pagination_info is None
|
assert "rel" in pagination_info or pagination_info is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestDropCampaignImageFallback:
|
||||||
|
"""Tests for DropCampaign image_best_url property with benefit fallback."""
|
||||||
|
|
||||||
|
def test_image_best_url_returns_campaign_image_url(self) -> None:
|
||||||
|
"""Test that image_best_url returns campaign image_url when present."""
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
assert campaign.image_best_url == "https://example.com/campaign.png"
|
||||||
|
|
||||||
|
def test_image_best_url_uses_benefit_image_when_campaign_has_no_image(self) -> None:
|
||||||
|
"""Test that image_best_url returns first benefit image when campaign has no image."""
|
||||||
|
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="", # No campaign image
|
||||||
|
)
|
||||||
|
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.image_best_url == "https://example.com/benefit.png"
|
||||||
|
|
||||||
|
def test_image_best_url_prefers_campaign_image_over_benefit_image(self) -> None:
|
||||||
|
"""Test that campaign image is preferred over benefit image."""
|
||||||
|
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", # Campaign has image
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Should return campaign image, not benefit image
|
||||||
|
assert campaign.image_best_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(
|
||||||
|
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="", # No campaign image
|
||||||
|
)
|
||||||
|
# No benefits or drops
|
||||||
|
|
||||||
|
assert not campaign.image_best_url
|
||||||
|
|
||||||
|
def test_image_best_url_uses_benefit_best_url(self) -> None:
|
||||||
|
"""Test that benefit's image_best_url property is used (prefers local file)."""
|
||||||
|
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="", # No campaign image
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Should use benefit's image_asset_url (since no local file)
|
||||||
|
assert campaign.image_best_url == benefit.image_best_url
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from django.core.paginator import PageNotAnInteger
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.core.serializers import serialize
|
from django.core.serializers import serialize
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
from django.db.models import Exists
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.db.models import OuterRef
|
from django.db.models import OuterRef
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
|
|
@ -856,7 +857,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
|
||||||
if campaign.description
|
if campaign.description
|
||||||
else f"Twitch drop campaign: {campaign_name}"
|
else f"Twitch drop campaign: {campaign_name}"
|
||||||
)
|
)
|
||||||
campaign_image: str | None = campaign.image_url
|
campaign_image: str | None = campaign.image_best_url
|
||||||
|
|
||||||
campaign_schema: dict[str, str | dict[str, str]] = {
|
campaign_schema: dict[str, str | dict[str, str]] = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
|
|
@ -1510,10 +1511,21 @@ def debug_view(request: HttpRequest) -> HttpResponse:
|
||||||
owners__isnull=True,
|
owners__isnull=True,
|
||||||
).order_by("display_name")
|
).order_by("display_name")
|
||||||
|
|
||||||
# Campaigns with missing or obviously broken images
|
# Campaigns with no images at all (no direct URL and no benefit image fallbacks)
|
||||||
broken_image_campaigns: QuerySet[DropCampaign] = DropCampaign.objects.filter(
|
broken_image_campaigns: QuerySet[DropCampaign] = (
|
||||||
Q(image_url__isnull=True) | Q(image_url__exact="") | ~Q(image_url__startswith="http"),
|
DropCampaign.objects
|
||||||
).select_related("game")
|
.filter(
|
||||||
|
Q(image_url__isnull=True) | Q(image_url__exact="") | ~Q(image_url__startswith="http"),
|
||||||
|
)
|
||||||
|
.exclude(
|
||||||
|
Exists(
|
||||||
|
TimeBasedDrop.objects.filter(campaign=OuterRef("pk")).filter(
|
||||||
|
benefits__image_asset_url__startswith="http",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.select_related("game")
|
||||||
|
)
|
||||||
|
|
||||||
# Benefits with missing images
|
# Benefits with missing images
|
||||||
broken_benefit_images: QuerySet[DropBenefit] = DropBenefit.objects.annotate(
|
broken_benefit_images: QuerySet[DropBenefit] = DropBenefit.objects.annotate(
|
||||||
|
|
@ -1544,13 +1556,20 @@ def debug_view(request: HttpRequest) -> HttpResponse:
|
||||||
.order_by("game__display_name", "name")
|
.order_by("game__display_name", "name")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Campaigns currently active but image missing
|
# Active campaigns with no images at all (no direct URL and no benefit image fallbacks)
|
||||||
active_missing_image: QuerySet[DropCampaign] = (
|
active_missing_image: QuerySet[DropCampaign] = (
|
||||||
DropCampaign.objects
|
DropCampaign.objects
|
||||||
.filter(start_at__lte=now, end_at__gte=now)
|
.filter(start_at__lte=now, end_at__gte=now)
|
||||||
.filter(
|
.filter(
|
||||||
Q(image_url__isnull=True) | Q(image_url__exact="") | ~Q(image_url__startswith="http"),
|
Q(image_url__isnull=True) | Q(image_url__exact="") | ~Q(image_url__startswith="http"),
|
||||||
)
|
)
|
||||||
|
.exclude(
|
||||||
|
Exists(
|
||||||
|
TimeBasedDrop.objects.filter(campaign=OuterRef("pk")).filter(
|
||||||
|
benefits__image_asset_url__startswith="http",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
.select_related("game")
|
.select_related("game")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue