diff --git a/templates/twitch/debug.html b/templates/twitch/debug.html
index 9c51f98..56f844f 100644
--- a/templates/twitch/debug.html
+++ b/templates/twitch/debug.html
@@ -6,12 +6,12 @@
Debug Data Integrity Report
Generated at: {{ now }}
- Games Without Campaigns / Orgs ({{ games_without_orgs|length }})
- {% if games_without_orgs %}
+ Games Without an Assigned Owner ({{ games_without_owner|length }})
+ {% if games_without_owner %}
@@ -25,7 +25,9 @@
@@ -33,6 +35,31 @@
None ✅
{% endif %}
+
+ Benefits With Broken Image URLs ({{ broken_benefit_images|length }})
+ {% if broken_benefit_images %}
+
+ {% for b in broken_benefit_images %}
+ {# A benefit is linked to a game via a drop and a campaign. #}
+ {# We use the 'with' tag to get the first drop for cleaner access. #}
+ {% with first_drop=b.drops.all.0 %}
+ -
+ {{ b.name }}
+ {# Check if the relationship path to the game exists #}
+ {% if first_drop and first_drop.campaign and first_drop.campaign.game %}
+ (Game: {{ first_drop.campaign.game.display_name }})
+ {% else %}
+ (Game: Not linked)
+ {% endif %}
+ - URL: {{ b.image_asset_url|default:'(empty)' }}
+
+ {% endwith %}
+ {% endfor %}
+
+ {% else %}
+ None ✅
+ {% endif %}
+
Active Campaigns Missing Image ({{ active_missing_image|length }})
{% if active_missing_image %}
@@ -40,20 +67,7 @@
{% for c in active_missing_image %}
{{ c.name }}
-
- {% endfor %}
-
- {% else %}
- None ✅
- {% endif %}
-
-
- Benefits With Broken Image URLs ({{ broken_benefit_images|length }})
- {% if broken_benefit_images %}
-
@@ -67,7 +81,9 @@
@@ -81,7 +97,9 @@
{% for c in invalid_date_campaigns %}
-
- {{ c.name }} - Start: {{ c.start_at|default:'(none)' }} / End: {{ c.end_at|default:'(none)' }}
+ {{ c.name }}
+ (Game: {{ c.game.display_name }})
+ - Start: {{ c.start_at|default:'(none)' }} / End: {{ c.end_at|default:'(none)' }}
{% endfor %}
@@ -96,14 +114,16 @@
| Game |
- Name |
+ Campaign Name |
Count |
{% for row in duplicate_name_campaigns %}
- | {{ row.game_id }} |
+
+ {{ row.game__display_name }}
+ |
{{ row.name }} |
{{ row.name_count }} |
diff --git a/twitch/views.py b/twitch/views.py
index 5ce30e0..560395f 100644
--- a/twitch/views.py
+++ b/twitch/views.py
@@ -7,9 +7,10 @@ from typing import TYPE_CHECKING, Any, cast
from django.contrib import messages
from django.contrib.auth.decorators import login_required
-from django.db import models
-from django.db.models import Count, Prefetch, Q
+from django.db.models import Count, F, Prefetch, Q
+from django.db.models.functions import Trim
from django.db.models.query import QuerySet
+from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
@@ -351,36 +352,56 @@ def debug_view(request: HttpRequest) -> HttpResponse:
"""
now = timezone.now()
- # Games with no organizations (no campaigns linking to an org)
- games_without_orgs: QuerySet[Game, Game] = Game.objects.filter(drop_campaigns__isnull=True).order_by("display_name")
+ # Games with no assigned owner organization
+ games_without_owner: QuerySet[Game] = Game.objects.filter(owner__isnull=True).order_by("display_name")
- # Campaigns with missing or obviously broken images (empty or very short or not http)
- broken_image_campaigns: QuerySet[DropCampaign, DropCampaign] = DropCampaign.objects.filter(
+ # Campaigns with missing or obviously broken images (empty or not starting with http)
+ broken_image_campaigns: QuerySet[DropCampaign] = DropCampaign.objects.filter(
Q(image_url__isnull=True) | Q(image_url__exact="") | ~Q(image_url__startswith="http")
).select_related("game")
# Benefits with missing images
- broken_benefit_images: QuerySet[DropBenefit, DropBenefit] = DropBenefit.objects.filter(
- Q(image_asset_url__isnull=True) | Q(image_asset_url__exact="") | ~Q(image_asset_url__startswith="http")
- ).prefetch_related(Prefetch("drops", queryset=TimeBasedDrop.objects.select_related("campaign__game")))
+ broken_benefit_images: QuerySet[DropBenefit] = (
+ DropBenefit.objects.annotate(
+ trimmed_url=Trim("image_asset_url") # Create a temporary field with no whitespace
+ )
+ .filter(
+ Q(image_asset_url__isnull=True)
+ | Q(trimmed_url__exact="") # Check the trimmed URL
+ | ~Q(image_asset_url__startswith="http")
+ )
+ .prefetch_related(
+ # Prefetch the path to the game to avoid N+1 queries in the template
+ Prefetch("drops", queryset=TimeBasedDrop.objects.select_related("campaign__game"))
+ )
+ )
# Time-based drops without any benefits
- drops_without_benefits: QuerySet[TimeBasedDrop, TimeBasedDrop] = TimeBasedDrop.objects.filter(benefits__isnull=True).select_related("campaign")
+ drops_without_benefits: QuerySet[TimeBasedDrop] = TimeBasedDrop.objects.filter(benefits__isnull=True).select_related("campaign__game")
# Campaigns with invalid dates (start after end or missing either)
- invalid_date_campaigns: QuerySet[DropCampaign, DropCampaign] = DropCampaign.objects.filter(
- Q(start_at__gt=models.F("end_at")) | Q(start_at__isnull=True) | Q(end_at__isnull=True)
+ invalid_date_campaigns: QuerySet[DropCampaign] = DropCampaign.objects.filter(
+ Q(start_at__gt=F("end_at")) | Q(start_at__isnull=True) | Q(end_at__isnull=True)
).select_related("game")
- # Duplicate campaign names per game
- duplicate_name_campaigns = DropCampaign.objects.values("game_id", "name").annotate(name_count=Count("id")).filter(name_count__gt=1).order_by("-name_count")
+ # Duplicate campaign names per game. We retrieve the game's name for user-friendly display.
+ duplicate_name_campaigns = (
+ DropCampaign.objects.values("game_id", "game__display_name", "name")
+ .annotate(name_count=Count("id"))
+ .filter(name_count__gt=1)
+ .order_by("game__display_name", "name")
+ )
# Campaigns currently active but image missing
- active_missing_image = DropCampaign.objects.filter(start_at__lte=now, end_at__gte=now).filter(Q(image_url__isnull=True) | Q(image_url__exact=""))
+ active_missing_image: QuerySet[DropCampaign] = (
+ DropCampaign.objects.filter(start_at__lte=now, end_at__gte=now)
+ .filter(Q(image_url__isnull=True) | Q(image_url__exact="") | ~Q(image_url__startswith="http"))
+ .select_related("game")
+ )
context: dict[str, Any] = {
"now": now,
- "games_without_orgs": games_without_orgs,
+ "games_without_owner": games_without_owner,
"broken_image_campaigns": broken_image_campaigns,
"broken_benefit_images": broken_benefit_images,
"drops_without_benefits": drops_without_benefits,