Instead of creating a "Unknown Organization", use None
This commit is contained in:
parent
5fe4ed4eb1
commit
70e2f4cebd
3 changed files with 125 additions and 12 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -58,6 +58,7 @@
|
|||
"thelovinator",
|
||||
"tqdm",
|
||||
"ttvdrops",
|
||||
"twid",
|
||||
"venv",
|
||||
"wrongpassword",
|
||||
"wsgi",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
120
twitch/management/commands/cleanup_unknown_organizations.py
Normal file
120
twitch/management/commands/cleanup_unknown_organizations.py
Normal 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
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue