Add debug view and import inventory JSON
This commit is contained in:
parent
5878ec186f
commit
bab2390109
6 changed files with 211 additions and 6 deletions
|
|
@ -48,6 +48,7 @@
|
||||||
<a href="{% url 'twitch:game_list' %}">Games</a> |
|
<a href="{% url 'twitch:game_list' %}">Games</a> |
|
||||||
<a href="{% url 'twitch:org_list' %}">Organizations</a> |
|
<a href="{% url 'twitch:org_list' %}">Organizations</a> |
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
<a href="{% url 'twitch:debug' %}">Debug</a> |
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
<a href="{% url 'admin:index' %}">Admin</a> |
|
<a href="{% url 'admin:index' %}">Admin</a> |
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
117
templates/twitch/debug.html
Normal file
117
templates/twitch/debug.html
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Debug
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Debug Data Integrity Report</h1>
|
||||||
|
<p>Generated at: {{ now }}</p>
|
||||||
|
<section>
|
||||||
|
<h2>Games Without Campaigns / Orgs ({{ games_without_orgs|length }})</h2>
|
||||||
|
{% if games_without_orgs %}
|
||||||
|
<ul>
|
||||||
|
{% for game in games_without_orgs %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'twitch:game_detail' game.id %}">{{ game.display_name }}</a> ({{ game.id }})
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>None ✅</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Campaigns With Broken Image URLs ({{ broken_image_campaigns|length }})</h2>
|
||||||
|
{% if broken_image_campaigns %}
|
||||||
|
<ul>
|
||||||
|
{% for c in broken_image_campaigns %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a> - {{ c.image_url|default:'(empty)' }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>None ✅</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Active Campaigns Missing Image ({{ active_missing_image|length }})</h2>
|
||||||
|
{% if active_missing_image %}
|
||||||
|
<ul>
|
||||||
|
{% for c in active_missing_image %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>None ✅</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Benefits With Broken Image URLs ({{ broken_benefit_images|length }})</h2>
|
||||||
|
{% if broken_benefit_images %}
|
||||||
|
<ul>
|
||||||
|
{% for b in broken_benefit_images %}
|
||||||
|
<li>
|
||||||
|
{{ b.name }} (Game: <a href="{% url 'twitch:game_detail' b.game.id %}">{{ b.game.display_name }}</a>) - {{ b.image_asset_url|default:'(empty)' }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>None ✅</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Time-Based Drops Without Benefits ({{ drops_without_benefits|length }})</h2>
|
||||||
|
{% if drops_without_benefits %}
|
||||||
|
<ul>
|
||||||
|
{% for d in drops_without_benefits %}
|
||||||
|
<li>
|
||||||
|
{{ d.name }} (Campaign: <a href="{% url 'twitch:campaign_detail' d.campaign.id %}">{{ d.campaign.name }}</a>)
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>None ✅</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Campaigns With Invalid Dates ({{ invalid_date_campaigns|length }})</h2>
|
||||||
|
{% if invalid_date_campaigns %}
|
||||||
|
<ul>
|
||||||
|
{% for c in invalid_date_campaigns %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a> - Start: {{ c.start_at|default:'(none)' }} / End: {{ c.end_at|default:'(none)' }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>None ✅</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Duplicate Campaign Names Per Game ({{ duplicate_name_campaigns|length }})</h2>
|
||||||
|
{% if duplicate_name_campaigns %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Game</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in duplicate_name_campaigns %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ row.game_id }}</td>
|
||||||
|
<td>{{ row.name }}</td>
|
||||||
|
<td>{{ row.name_count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>None ✅</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% endblock content %}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import traceback
|
import traceback
|
||||||
|
|
@ -19,6 +20,9 @@ if TYPE_CHECKING:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""Import Twitch drop campaign data from a JSON file or directory of JSON files."""
|
"""Import Twitch drop campaign data from a JSON file or directory of JSON files."""
|
||||||
|
|
||||||
|
|
@ -113,6 +117,7 @@ class Command(BaseCommand):
|
||||||
processed_path: Subdirectory to move processed files to.
|
processed_path: Subdirectory to move processed files to.
|
||||||
"""
|
"""
|
||||||
data = orjson.loads(file_path.read_bytes())
|
data = orjson.loads(file_path.read_bytes())
|
||||||
|
|
||||||
broken_dir: Path = processed_path / "broken"
|
broken_dir: Path = processed_path / "broken"
|
||||||
broken_dir.mkdir(parents=True, exist_ok=True)
|
broken_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
@ -128,14 +133,13 @@ class Command(BaseCommand):
|
||||||
"DropCurrentSessionContext",
|
"DropCurrentSessionContext",
|
||||||
"DropsHighlightService_AvailableDrops",
|
"DropsHighlightService_AvailableDrops",
|
||||||
"DropsPage_ClaimDropRewards",
|
"DropsPage_ClaimDropRewards",
|
||||||
"Inventory",
|
|
||||||
"OnsiteNotifications_DeleteNotification",
|
"OnsiteNotifications_DeleteNotification",
|
||||||
"PlaybackAccessToken",
|
"PlaybackAccessToken",
|
||||||
"streamPlaybackAccessToken",
|
"streamPlaybackAccessToken",
|
||||||
"VideoPlayerStreamInfoOverlayChannel",
|
"VideoPlayerStreamInfoOverlayChannel",
|
||||||
]
|
]
|
||||||
for keyword in probably_shit:
|
for keyword in probably_shit:
|
||||||
if f'"operationName": "{keyword}"' in data:
|
if f'"operationName": "{keyword}"' in str(data):
|
||||||
target_dir: Path = broken_dir / keyword
|
target_dir: Path = broken_dir / keyword
|
||||||
target_dir.mkdir(parents=True, exist_ok=True)
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
@ -265,6 +269,13 @@ class Command(BaseCommand):
|
||||||
elif isinstance(campaigns, dict):
|
elif isinstance(campaigns, dict):
|
||||||
self.import_to_db(campaigns, file_path=file_path)
|
self.import_to_db(campaigns, file_path=file_path)
|
||||||
|
|
||||||
|
elif "currentUser" in data["data"] and "inventory" in data["data"]["currentUser"]:
|
||||||
|
inventory = data["data"]["currentUser"]["inventory"]
|
||||||
|
if "dropCampaignsInProgress" in inventory:
|
||||||
|
campaigns = inventory["dropCampaignsInProgress"]
|
||||||
|
for drop_campaign_data in campaigns:
|
||||||
|
self.import_to_db(drop_campaign_data, file_path=file_path)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
msg = "Invalid JSON structure: Missing either data.user.dropCampaign or data.currentUser.dropCampaigns"
|
msg = "Invalid JSON structure: Missing either data.user.dropCampaign or data.currentUser.dropCampaigns"
|
||||||
raise CommandError(msg)
|
raise CommandError(msg)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import ClassVar
|
from typing import TYPE_CHECKING, ClassVar
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from accounts.models import User
|
from accounts.models import User
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import datetime
|
||||||
|
|
||||||
logger: logging.Logger = logging.getLogger("ttvdrops")
|
logger: logging.Logger = logging.getLogger("ttvdrops")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -97,7 +100,9 @@ class DropCampaign(models.Model):
|
||||||
@property
|
@property
|
||||||
def is_active(self) -> bool:
|
def is_active(self) -> bool:
|
||||||
"""Check if the campaign is currently active."""
|
"""Check if the campaign is currently active."""
|
||||||
now = timezone.now()
|
now: datetime.datetime = timezone.now()
|
||||||
|
if self.start_at is None or self.end_at is None:
|
||||||
|
return False
|
||||||
return self.start_at <= now <= self.end_at
|
return self.start_at <= now <= self.end_at
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ app_name = "twitch"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.dashboard, name="dashboard"),
|
path("", views.dashboard, name="dashboard"),
|
||||||
|
path("debug/", views.debug_view, name="debug"),
|
||||||
path("campaigns/", views.DropCampaignListView.as_view(), name="campaign_list"),
|
path("campaigns/", views.DropCampaignListView.as_view(), name="campaign_list"),
|
||||||
path("campaigns/<str:pk>/", views.DropCampaignDetailView.as_view(), name="campaign_detail"),
|
path("campaigns/<str:pk>/", views.DropCampaignDetailView.as_view(), name="campaign_detail"),
|
||||||
path("games/", views.GameListView.as_view(), name="game_list"),
|
path("games/", views.GameListView.as_view(), name="game_list"),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
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, Prefetch, Q
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.http.response import HttpResponseRedirect
|
from django.http.response import HttpResponseRedirect
|
||||||
|
|
@ -14,7 +15,14 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
from twitch.models import DropCampaign, Game, NotificationSubscription, Organization, TimeBasedDrop
|
from twitch.models import (
|
||||||
|
DropBenefit,
|
||||||
|
DropCampaign,
|
||||||
|
Game,
|
||||||
|
NotificationSubscription,
|
||||||
|
Organization,
|
||||||
|
TimeBasedDrop,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
|
@ -286,7 +294,9 @@ class GameDetailView(DetailView):
|
||||||
)
|
)
|
||||||
|
|
||||||
active_campaigns: list[DropCampaign] = [
|
active_campaigns: list[DropCampaign] = [
|
||||||
campaign for campaign in all_campaigns if campaign.start_at <= now and campaign.end_at is not None and campaign.end_at >= now
|
campaign
|
||||||
|
for campaign in all_campaigns
|
||||||
|
if campaign.start_at is not None and campaign.start_at <= now and campaign.end_at is not None and campaign.end_at >= now
|
||||||
]
|
]
|
||||||
active_campaigns.sort(key=lambda c: c.end_at if c.end_at is not None else datetime.datetime.max.replace(tzinfo=datetime.UTC))
|
active_campaigns.sort(key=lambda c: c.end_at if c.end_at is not None else datetime.datetime.max.replace(tzinfo=datetime.UTC))
|
||||||
|
|
||||||
|
|
@ -365,6 +375,66 @@ def dashboard(request: HttpRequest) -> HttpResponse:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def debug_view(request: HttpRequest) -> HttpResponse:
|
||||||
|
"""Debug view showing potentially broken or inconsistent data.
|
||||||
|
|
||||||
|
Only staff users may access this endpoint.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HttpResponse: Rendered debug template or redirect if unauthorized.
|
||||||
|
"""
|
||||||
|
# Was previously staff-only; now any authenticated user can view.
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Campaigns with missing or obviously broken images (empty or very short or not http)
|
||||||
|
broken_image_campaigns: QuerySet[DropCampaign, DropCampaign] = DropCampaign.objects.filter(
|
||||||
|
Q(image_url__isnull=True) | Q(image_url__exact="") | ~Q(image_url__startswith="http")
|
||||||
|
).select_related("game", "owner")
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
).select_related("game", "owner_organization")
|
||||||
|
|
||||||
|
# Time-based drops without any benefits
|
||||||
|
drops_without_benefits: QuerySet[TimeBasedDrop, TimeBasedDrop] = TimeBasedDrop.objects.filter(benefits__isnull=True).select_related(
|
||||||
|
"campaign"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
).select_related("game", "owner")
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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="")
|
||||||
|
)
|
||||||
|
|
||||||
|
context: dict[str, Any] = {
|
||||||
|
"now": now,
|
||||||
|
"games_without_orgs": games_without_orgs,
|
||||||
|
"broken_image_campaigns": broken_image_campaigns,
|
||||||
|
"broken_benefit_images": broken_benefit_images,
|
||||||
|
"drops_without_benefits": drops_without_benefits,
|
||||||
|
"invalid_date_campaigns": invalid_date_campaigns,
|
||||||
|
"duplicate_name_campaigns": duplicate_name_campaigns,
|
||||||
|
"active_missing_image": active_missing_image,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "twitch/debug.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def subscribe_game_notifications(request: HttpRequest, game_id: str) -> HttpResponseRedirect:
|
def subscribe_game_notifications(request: HttpRequest, game_id: str) -> HttpResponseRedirect:
|
||||||
"""Update Game notification for a user.
|
"""Update Game notification for a user.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue