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",
|
"thelovinator",
|
||||||
"tqdm",
|
"tqdm",
|
||||||
"ttvdrops",
|
"ttvdrops",
|
||||||
|
"twid",
|
||||||
"venv",
|
"venv",
|
||||||
"wrongpassword",
|
"wrongpassword",
|
||||||
"wsgi",
|
"wsgi",
|
||||||
|
|
|
||||||
|
|
@ -438,7 +438,7 @@ class Command(BaseCommand):
|
||||||
def _get_or_create_game(
|
def _get_or_create_game(
|
||||||
self,
|
self,
|
||||||
game_data: GameSchema,
|
game_data: GameSchema,
|
||||||
campaign_org_obj: Organization,
|
campaign_org_obj: Organization | None,
|
||||||
) -> Game:
|
) -> Game:
|
||||||
"""Get or create a game from cache or database, using correct owner organization.
|
"""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):
|
if isinstance(owner_org_data, dict):
|
||||||
owner_org_data = OrganizationSchema.model_validate(owner_org_data)
|
owner_org_data = OrganizationSchema.model_validate(owner_org_data)
|
||||||
owner_orgs.add(self._get_or_create_organization(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:
|
if campaign_org_obj:
|
||||||
owner_orgs.add(campaign_org_obj)
|
owner_orgs.add(campaign_org_obj)
|
||||||
|
|
||||||
|
|
@ -628,17 +628,9 @@ class Command(BaseCommand):
|
||||||
for drop_campaign in campaigns_to_process:
|
for drop_campaign in campaigns_to_process:
|
||||||
# Handle campaigns without owner (e.g., from Inventory operation)
|
# Handle campaigns without owner (e.g., from Inventory operation)
|
||||||
owner_data: OrganizationSchema | None = getattr(drop_campaign, "owner", None)
|
owner_data: OrganizationSchema | None = getattr(drop_campaign, "owner", None)
|
||||||
|
org_obj: Organization | None = None
|
||||||
if owner_data:
|
if owner_data:
|
||||||
org_obj: Organization = self._get_or_create_organization(
|
org_obj = self._get_or_create_organization(org_data=owner_data)
|
||||||
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
|
|
||||||
|
|
||||||
game_obj: Game = self._get_or_create_game(
|
game_obj: Game = self._get_or_create_game(
|
||||||
game_data=drop_campaign.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