Instead of creating a "Unknown Organization", use None

This commit is contained in:
Joakim Hellsén 2026-01-12 01:25:43 +01:00
commit 70e2f4cebd
No known key found for this signature in database
3 changed files with 125 additions and 12 deletions

View file

@ -58,6 +58,7 @@
"thelovinator",
"tqdm",
"ttvdrops",
"twid",
"venv",
"wrongpassword",
"wsgi",

View file

@ -438,7 +438,7 @@ class Command(BaseCommand):
def _get_or_create_game(
self,
game_data: GameSchema,
campaign_org_obj: Organization,
campaign_org_obj: Organization | None,
) -> Game:
"""Get or create a game from cache or database, using correct owner organization.
@ -456,7 +456,7 @@ class Command(BaseCommand):
if isinstance(owner_org_data, dict):
owner_org_data = OrganizationSchema.model_validate(owner_org_data)
owner_orgs.add(self._get_or_create_organization(owner_org_data))
# Always add campaign_org_obj as fallback
# Add campaign organization as fallback only when provided
if campaign_org_obj:
owner_orgs.add(campaign_org_obj)
@ -628,17 +628,9 @@ class Command(BaseCommand):
for drop_campaign in campaigns_to_process:
# Handle campaigns without owner (e.g., from Inventory operation)
owner_data: OrganizationSchema | None = getattr(drop_campaign, "owner", None)
org_obj: Organization | None = None
if owner_data:
org_obj: Organization = self._get_or_create_organization(
org_data=owner_data,
)
else:
# Create a default organization for campaigns without owner
org_obj, _ = Organization.objects.get_or_create(
twitch_id="unknown",
defaults={"name": "Unknown Organization"},
)
self.organization_cache["unknown"] = org_obj
org_obj = self._get_or_create_organization(org_data=owner_data)
game_obj: Game = self._get_or_create_game(
game_data=drop_campaign.game,

View file

@ -0,0 +1,120 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import Any
from colorama import Fore
from colorama import Style
from colorama import init as colorama_init
from django.core.management.base import BaseCommand
from django.core.management.base import CommandError
from django.core.management.base import CommandParser
from twitch.models import Game
from twitch.models import Organization
if TYPE_CHECKING:
from debug_toolbar.panels.templates.panel import QuerySet
class Command(BaseCommand):
"""Detach the placeholder 'Unknown Organization' from games and optionally re-import data.
This command removes ManyToMany relationships between the special
organization (default twitch_id='unknown') and any games it was attached to
as a fallback. Organizations in Twitch data are optional; this cleanup
ensures we don't over-assign a fake org.
"""
help = "Detach 'Unknown Organization' from games and optionally re-import"
requires_migrations_checks = True
def add_arguments(self, parser: CommandParser) -> None:
"""Define CLI arguments for the command."""
parser.add_argument(
"--org-id",
dest="org_id",
default="unknown",
help="Organization twitch_id to detach (default: 'unknown')",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Do not modify the database; print what would change.",
)
parser.add_argument(
"--delete-empty-org",
action="store_true",
help=(
"Delete the organization after detaching if it has no games remaining. "
"Only applies when not in --dry-run."
),
)
def handle(self, *args: Any, **options: Any) -> None: # noqa: ANN401, ARG002
"""Execute the command to detach the organization and optionally re-import data.
Args:
*args: Positional arguments (unused).
**options: Command-line options parsed by argparse.
Raises:
CommandError: If the specified organization does not exist.
"""
colorama_init(autoreset=True)
org_id: str = options["org_id"]
dry_run: bool = bool(options.get("dry_run"))
delete_empty_org: bool = bool(options.get("delete_empty_org"))
try:
org: Organization = Organization.objects.get(twitch_id=org_id)
except Organization.DoesNotExist as exc: # pragma: no cover - simple guard
msg: str = f"Organization with twitch_id='{org_id}' does not exist. Nothing to do."
raise CommandError(msg) from exc
# Compute the set of affected games via the through relation for accuracy and performance
affected_games_qs: QuerySet[Game, Game] = Game.objects.filter(owners=org).order_by("display_name")
affected_count: int = affected_games_qs.count()
if affected_count == 0:
self.stdout.write(
f"{Fore.YELLOW}{Style.RESET_ALL} No games linked to organization '{org.name}' ({org.twitch_id}).",
)
else:
self.stdout.write(
f"{Fore.CYAN}{Style.RESET_ALL} Found {affected_count:,} game(s) linked to '{org.name}' ({org.twitch_id}).", # noqa: E501
)
# Show a short preview list in dry-run mode
preview_limit = 10
if affected_count > 0 and dry_run:
sample: list[Game] = list(affected_games_qs[:preview_limit])
for g in sample:
self.stdout.write(f" - {g.display_name} [{g.twitch_id}]")
if affected_count > preview_limit:
self.stdout.write(f" ... and {affected_count - preview_limit:,} more")
# Perform the detachment
if affected_count > 0 and not dry_run:
# Use the reverse relation clear() to efficiently remove all M2M rows for this org
# This issues a single DELETE on the through table for organization=org
org.games.clear() # pyright: ignore[reportAttributeAccessIssue]
self.stdout.write(
f"{Fore.GREEN}{Style.RESET_ALL} Detached organization '{org.name}' from {affected_count:,} game(s).",
)
# Optionally delete the organization if it no longer has games
if not dry_run and delete_empty_org:
remaining_games: int = org.games.count() # pyright: ignore[reportAttributeAccessIssue]
if remaining_games == 0:
org_name: str = org.name
org_twid: str = org.twitch_id
org.delete()
self.stdout.write(
f"{Fore.GREEN}{Style.RESET_ALL} Deleted organization '{org_name}' ({org_twid}) as it has no games.", # noqa: E501
)
else:
self.stdout.write(
f"{Fore.YELLOW}{Style.RESET_ALL} Organization '{org.name}' still has {remaining_games:,} game(s); not deleted.", # noqa: E501
)