"""Füllt Artenkatalog und Abzeichen. Idempotent — beliebig oft ausführbar.""" from __future__ import annotations import sys from pathlib import Path from django.core.management.base import BaseCommand from dex import scoring from dex.models import Species def _steckbrief(z) -> dict: """Steckbrief-Felder aus einem Zusatz-Eintrag, oder leer.""" if z is None: return {} return { "kategorie": z.kategorie, "beschreibung": z.text, "nahrung": z.nahrung, "lebensraum": z.lebensraum, "stimme": z.stimme, "kategorie_en": z.kategorie_en, "beschreibung_en": z.text_en, "nahrung_en": z.nahrung_en, "lebensraum_en": z.lebensraum_en, "stimme_en": z.stimme_en, } # Muss zu RARITY_XP in detector/species_de.py passen. RARITY_XP = { "haeufig": 10, "gelegentlich": 25, "selten": 60, "episch": 150, "legendaer": 400, } def find_catalog_root() -> Path | None: """Sucht das Verzeichnis, das ``detector/species_de.py`` enthält. Lokal liegt es eine Ebene über ``web/``, im Container direkt neben ``manage.py`` — statt beide Fälle zu raten, einfach aufwärts suchen. """ for base in Path(__file__).resolve().parents: if (base / "detector" / "species_de.py").exists(): return base return None class Command(BaseCommand): help = "Legt Arten und Abzeichen an bzw. aktualisiert sie." def handle(self, *args, **options): # detector.species_de ist die einzige Quelle der Wahrheit für den # Katalog. Das Modul hängt nur an der Stdlib, der Import kostet die # Web-App also keine schweren Abhängigkeiten. root = find_catalog_root() if root is None: self.stderr.write( self.style.ERROR( "detector/species_de.py nicht gefunden — " "weder neben manage.py noch in einem übergeordneten Verzeichnis." ) ) return sys.path.insert(0, str(root)) try: from detector.species_de import SPECIES except ImportError as exc: self.stderr.write(self.style.ERROR(f"Artenkatalog nicht ladbar: {exc}")) return try: from detector.species_extra import EXTRA except ImportError: EXTRA = [] try: from detector.species_info import INFO except ImportError: INFO = {} self.stdout.write( self.style.WARNING("Keine Steckbriefe gefunden — Arten bleiben knapp.") ) try: from detector.species_info_en import INFO_EN except ImportError: INFO_EN = {} # Steckbriefe fuer die erweiterten Arten — beide Sprachen in einem # Datensatz, weil sie zusammen geschrieben werden. try: from detector.profiles import INFO_EXTRA except ImportError: INFO_EXTRA = {} # Die Dex-Nummer soll stabil bleiben, damit #012 immer dieselbe Art # ist. Sortiert nach Seltenheit, dann alphabetisch — neue Arten # verschieben also Nummern, aber nur innerhalb ihrer Stufe. order = {"haeufig": 0, "gelegentlich": 1, "selten": 2, "episch": 3, "legendaer": 4} # Kernarten zuerst, dann die Erweiterung — so behalten die Gartenvögel # ihre niedrigen Dex-Nummern, auch wenn der Katalog wächst. geordnet = sorted(SPECIES, key=lambda s: (order.get(s.rarity, 9), s.german)) geordnet_extra = sorted(EXTRA, key=lambda e: (order.get(e.rarity, 9), e.german)) created = updated = mit_text = 0 for nummer, s in enumerate(geordnet, start=1): info = INFO.get(s.slug) defaults = { "german": s.german, "scientific": s.scientific, "english": s.english, "rarity": s.rarity, "xp": s.xp, "fact": s.fact, "size_cm": s.size_cm, "number": nummer, "lebensraum_typ": s.lebensraum, "regions": ["de"], } if info is not None: mit_text += 1 defaults |= { "typ": info.typ, "kategorie": info.kategorie, "beschreibung": info.text, "gewicht_g": info.gewicht_g, "spannweite_cm": info.spannweite_cm, "zug": info.zug, "nahrung": info.nahrung, "lebensraum": info.lebensraum, "stimme": info.stimme, # Verweise auf Arten, die es (noch) nicht gibt, fliegen raus — # sonst zeigt die Seite Links ins Leere. "verwechslung": [ v for v in info.verwechslung if any(x.slug == v for x in SPECIES) ], } info_en = INFO_EN.get(s.slug) if info_en is not None: defaults |= { "kategorie_en": info_en.kategorie, "beschreibung_en": info_en.text, "nahrung_en": info_en.nahrung, "lebensraum_en": info_en.lebensraum, "stimme_en": info_en.stimme, } _, was_created = Species.objects.update_or_create(slug=s.slug, defaults=defaults) created += was_created updated += not was_created # Erweiterte Arten: nur Strukturdaten, kein Erzähltext. Vorhandene # Texte werden nicht überschrieben — wer einen Steckbrief nachträgt, # verliert ihn beim nächsten Seed nicht. naechste = len(geordnet) for offset, e in enumerate(geordnet_extra, start=1): obj, was_created = Species.objects.update_or_create( slug=e.slug, defaults={ "german": e.german, "scientific": e.scientific, "english": e.english, "rarity": e.rarity, "xp": RARITY_XP.get(e.rarity, 10), "size_cm": e.size_cm, "number": naechste + offset, "typ": e.typ, "gewicht_g": e.gewicht_g, "spannweite_cm": e.spannweite_cm, "zug": e.zug, "lebensraum_typ": e.lebensraum, "regions": ["de"], } | _steckbrief(INFO_EXTRA.get(e.slug)), ) created += was_created updated += not was_created badges = scoring.sync_badges() gesamt = len(SPECIES) + len(EXTRA) mit_text += sum(1 for e in EXTRA if e.slug in INFO_EXTRA) mit_en = sum(1 for s in SPECIES if s.slug in INFO_EN) mit_en += sum(1 for e in EXTRA if e.slug in INFO_EXTRA) ohne_text = gesamt - mit_text self.stdout.write( self.style.SUCCESS( f"Arten: {created} neu, {updated} aktualisiert, " f"{mit_text} von {gesamt} mit Steckbrief ({mit_en} auch auf Englisch). " f"Abzeichen: {badges} neu." ) ) if ohne_text: self.stdout.write( self.style.WARNING(f"{ohne_text} Arten noch ohne Steckbrief") )