Add is_fully_imported field to DropCampaign and KickDropCampaign models; update views and commands to filter by this field
All checks were successful
Deploy to Server / deploy (push) Successful in 18s
All checks were successful
Deploy to Server / deploy (push) Successful in 18s
This commit is contained in:
parent
5d56a936b0
commit
a8747791c0
12 changed files with 242 additions and 13 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
|
@ -15,6 +16,7 @@ from colorama import Fore
|
|||
from colorama import Style
|
||||
from colorama import init as colorama_init
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.base import CommandError
|
||||
from pydantic import ValidationError
|
||||
|
|
@ -35,6 +37,8 @@ from twitch.utils import normalize_twitch_box_art_url
|
|||
from twitch.utils import parse_date
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
from django.core.management.base import CommandParser
|
||||
from django.db.models import Model
|
||||
from json_repair import JSONReturnType
|
||||
|
|
@ -545,7 +549,7 @@ class Command(BaseCommand):
|
|||
|
||||
return valid_responses, broken_dir
|
||||
|
||||
def _save_if_changed(self, obj: Model, defaults: dict[str, object]) -> bool:
|
||||
def _save_if_changed(self, obj: Model, defaults: Mapping[str, object]) -> bool:
|
||||
"""Save the model instance only when data actually changed.
|
||||
|
||||
This prevents unnecessary updates and avoids touching fields like
|
||||
|
|
@ -618,7 +622,7 @@ class Command(BaseCommand):
|
|||
if campaign_org_obj:
|
||||
owner_orgs.add(campaign_org_obj)
|
||||
|
||||
defaults: dict[str, str] = {
|
||||
defaults: dict[str, object] = {
|
||||
"display_name": game_data.display_name or (game_data.name or ""),
|
||||
"name": game_data.name or "",
|
||||
"slug": game_data.slug or "",
|
||||
|
|
@ -681,7 +685,7 @@ class Command(BaseCommand):
|
|||
# Use name as display_name fallback if displayName is None
|
||||
display_name: str = channel_info.display_name or channel_info.name
|
||||
|
||||
defaults: dict[str, str] = {
|
||||
defaults: dict[str, object] = {
|
||||
"name": channel_info.name,
|
||||
"display_name": display_name,
|
||||
}
|
||||
|
|
@ -698,7 +702,7 @@ class Command(BaseCommand):
|
|||
|
||||
return channel_obj
|
||||
|
||||
def process_responses(
|
||||
def process_responses( # noqa: PLR0915
|
||||
self,
|
||||
responses: list[dict[str, Any]],
|
||||
file_path: Path,
|
||||
|
|
@ -778,7 +782,7 @@ class Command(BaseCommand):
|
|||
raise ValueError(msg)
|
||||
continue
|
||||
|
||||
defaults: dict[str, str | datetime | Game | bool] = {
|
||||
defaults: dict[str, object] = {
|
||||
"name": drop_campaign.name,
|
||||
"description": drop_campaign.description,
|
||||
"image_url": drop_campaign.image_url,
|
||||
|
|
@ -800,6 +804,16 @@ class Command(BaseCommand):
|
|||
f"{Fore.GREEN}✓{Style.RESET_ALL} Created new campaign: {drop_campaign.name}",
|
||||
)
|
||||
|
||||
# Always run additional commands after import
|
||||
call_command("download_box_art")
|
||||
call_command("download_campaign_images")
|
||||
call_command("convert_images_to_modern_formats")
|
||||
|
||||
# After all downloads and processing, mark as fully imported
|
||||
if campaign_obj:
|
||||
campaign_obj.is_fully_imported = True
|
||||
campaign_obj.save(update_fields=["is_fully_imported"])
|
||||
|
||||
action: Literal["Imported new", "Updated"] = (
|
||||
"Imported new" if created else "Updated"
|
||||
)
|
||||
|
|
@ -1026,7 +1040,7 @@ class Command(BaseCommand):
|
|||
f"{Fore.YELLOW}→{Style.RESET_ALL} Game not found for reward campaign: {game_id}",
|
||||
)
|
||||
|
||||
defaults: dict[str, str | datetime | Game | bool | None] = {
|
||||
defaults: dict[str, object] = {
|
||||
"name": reward_campaign.name,
|
||||
"brand": reward_campaign.brand,
|
||||
"starts_at": starts_at_dt,
|
||||
|
|
@ -1084,6 +1098,7 @@ class Command(BaseCommand):
|
|||
else:
|
||||
msg: str = f"Path does not exist: {input_path}"
|
||||
raise CommandError(msg)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
tqdm.write(self.style.WARNING("\n\nInterrupted by user!"))
|
||||
tqdm.write(self.style.WARNING("Shutting down gracefully..."))
|
||||
|
|
|
|||
23
twitch/migrations/0015_dropcampaign_is_fully_imported.py
Normal file
23
twitch/migrations/0015_dropcampaign_is_fully_imported.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 6.0.3 on 2026-03-19 19:20
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""Add a new field `is_fully_imported` to the `DropCampaign` model to track whether all related images and formats have been imported and are ready for display. This field will help us filter out campaigns that are not yet fully imported in our views and APIs, ensuring that only complete campaigns are shown to users."""
|
||||
|
||||
dependencies = [
|
||||
("twitch", "0014_dropcampaign_image_mime_type_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="dropcampaign",
|
||||
name="is_fully_imported",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="True if all images and formats are imported and ready for display.",
|
||||
),
|
||||
),
|
||||
]
|
||||
25
twitch/migrations/0016_mark_all_drops_fully_imported.py
Normal file
25
twitch/migrations/0016_mark_all_drops_fully_imported.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
def mark_all_drops_fully_imported(apps, schema_editor) -> None: # noqa: ANN001, ARG001
|
||||
"""Marks all existing DropCampaigns as fully imported.
|
||||
|
||||
This was needed to ensure that the Twitch API view only returns campaigns that are ready for display.
|
||||
"""
|
||||
DropCampaign = apps.get_model("twitch", "DropCampaign")
|
||||
DropCampaign.objects.all().update(is_fully_imported=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""Marks all existing DropCampaigns as fully imported.
|
||||
|
||||
This was needed to ensure that the Twitch API view only returns campaigns that are ready for display.
|
||||
"""
|
||||
|
||||
dependencies = [
|
||||
("twitch", "0015_dropcampaign_is_fully_imported"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(mark_all_drops_fully_imported),
|
||||
]
|
||||
|
|
@ -429,6 +429,10 @@ class DropCampaign(auto_prefetch.Model):
|
|||
auto_now=True,
|
||||
help_text="Timestamp when this campaign record was last updated.",
|
||||
)
|
||||
is_fully_imported = models.BooleanField(
|
||||
default=False,
|
||||
help_text="True if all images and formats are imported and ready for display.",
|
||||
)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
ordering = ["-start_at"]
|
||||
|
|
|
|||
|
|
@ -566,6 +566,7 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=10),
|
||||
end_at=now + timedelta(days=10),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
for i in range(150)
|
||||
]
|
||||
|
|
@ -609,6 +610,7 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=5),
|
||||
end_at=now + timedelta(days=5),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Create upcoming campaign
|
||||
|
|
@ -619,6 +621,7 @@ class TestChannelListView:
|
|||
start_at=now + timedelta(days=5),
|
||||
end_at=now + timedelta(days=10),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Create expired campaign
|
||||
|
|
@ -629,6 +632,7 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=10),
|
||||
end_at=now - timedelta(days=5),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Test active filter
|
||||
|
|
@ -648,7 +652,7 @@ class TestChannelListView:
|
|||
name="Game",
|
||||
display_name="Game",
|
||||
)
|
||||
now: datetime.datetime = timezone.now()
|
||||
now = timezone.now()
|
||||
|
||||
# Create active campaign
|
||||
DropCampaign.objects.create(
|
||||
|
|
@ -658,16 +662,18 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=5),
|
||||
end_at=now + timedelta(days=5),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Create upcoming campaign
|
||||
_upcoming_campaign: DropCampaign = DropCampaign.objects.create(
|
||||
DropCampaign.objects.create(
|
||||
twitch_id="upcoming",
|
||||
name="Upcoming Campaign",
|
||||
game=game,
|
||||
start_at=now + timedelta(days=5),
|
||||
start_at=now + timedelta(days=1),
|
||||
end_at=now + timedelta(days=10),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Create expired campaign
|
||||
|
|
@ -678,6 +684,7 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=10),
|
||||
end_at=now - timedelta(days=5),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Test upcoming filter
|
||||
|
|
@ -707,6 +714,7 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=5),
|
||||
end_at=now + timedelta(days=5),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Create upcoming campaign
|
||||
|
|
@ -717,6 +725,7 @@ class TestChannelListView:
|
|||
start_at=now + timedelta(days=5),
|
||||
end_at=now + timedelta(days=10),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Create expired campaign
|
||||
|
|
@ -727,6 +736,7 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=10),
|
||||
end_at=now - timedelta(days=5),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Test expired filter
|
||||
|
|
@ -761,6 +771,7 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=5),
|
||||
end_at=now + timedelta(days=5),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
DropCampaign.objects.create(
|
||||
twitch_id="c2",
|
||||
|
|
@ -769,6 +780,7 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=5),
|
||||
end_at=now + timedelta(days=5),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Create campaign for game 2
|
||||
|
|
@ -779,6 +791,7 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=5),
|
||||
end_at=now + timedelta(days=5),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
# Test filtering by game1
|
||||
|
|
@ -819,6 +832,7 @@ class TestChannelListView:
|
|||
start_at=now - timedelta(days=5),
|
||||
end_at=now + timedelta(days=5),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
for i in range(150)
|
||||
]
|
||||
|
|
@ -1337,6 +1351,7 @@ class TestSitemapView:
|
|||
operation_names=["DropCampaignDetails"],
|
||||
start_at=now - datetime.timedelta(days=1),
|
||||
end_at=now + datetime.timedelta(days=1),
|
||||
is_fully_imported=True,
|
||||
)
|
||||
inactive_campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="camp2",
|
||||
|
|
@ -1346,6 +1361,7 @@ class TestSitemapView:
|
|||
operation_names=["DropCampaignDetails"],
|
||||
start_at=now - datetime.timedelta(days=10),
|
||||
end_at=now - datetime.timedelta(days=5),
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
kick_org: KickOrganization = KickOrganization.objects.create(
|
||||
|
|
@ -1364,6 +1380,7 @@ class TestSitemapView:
|
|||
category=kick_cat,
|
||||
starts_at=now - datetime.timedelta(days=1),
|
||||
ends_at=now + datetime.timedelta(days=1),
|
||||
is_fully_imported=True,
|
||||
)
|
||||
kick_inactive: KickDropCampaign = KickDropCampaign.objects.create(
|
||||
kick_id="kcamp2",
|
||||
|
|
@ -1372,6 +1389,7 @@ class TestSitemapView:
|
|||
category=kick_cat,
|
||||
starts_at=now - datetime.timedelta(days=10),
|
||||
ends_at=now - datetime.timedelta(days=5),
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
badge: ChatBadgeSet = ChatBadgeSet.objects.create(set_id="badge1")
|
||||
|
|
@ -1707,6 +1725,7 @@ class TestSEOPaginationLinks:
|
|||
description="Desc",
|
||||
game=game,
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
response = client.get(reverse("twitch:campaign_list"))
|
||||
|
|
@ -1731,6 +1750,7 @@ class TestSEOPaginationLinks:
|
|||
description="Desc",
|
||||
game=game,
|
||||
operation_names=["DropCampaignDetails"],
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
response = client.get(reverse("twitch:campaign_list"))
|
||||
|
|
|
|||
|
|
@ -430,7 +430,9 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0
|
|||
game_filter: str | None = request.GET.get("game")
|
||||
status_filter: str | None = request.GET.get("status")
|
||||
per_page: int = 100
|
||||
queryset: QuerySet[DropCampaign] = DropCampaign.objects.all()
|
||||
queryset: QuerySet[DropCampaign] = DropCampaign.objects.filter(
|
||||
is_fully_imported=True,
|
||||
)
|
||||
|
||||
if game_filter:
|
||||
queryset = queryset.filter(game__twitch_id=game_filter)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue