from django.conf import settings from django.contrib import admin, messages from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from . import benachrichtigung as bn from .models import (Badge, Benachrichtigung, Camera, DexEntry, EarnedBadge, Sighting, Species) @admin.register(Camera) class CameraAdmin(admin.ModelAdmin): list_display = ( "name", "slug", "location", "active", "last_seen", "health", "live", ) list_filter = ("active", "live_enabled") # Der Token existiert hier nicht im Klartext — nur sein Hash. Neuen Token # gibt es über 'manage.py camera rotate '. readonly_fields = ("token_hash", "created_at", "last_seen", "letzter_kontakt") prepopulated_fields = {"slug": ("name",)} fieldsets = ( (None, {"fields": ("name", "slug", "location", "active")}), ("Erkennung", { "fields": ("stream_url", "habitats", "detect_threshold", "min_bird_score"), "description": _( "Der Detector holt sich diese Werte selbst — er fragt " "/api/config/ mit seinem Token und übernimmt " "Änderungen im laufenden Betrieb, ohne Neustart. Eine " "Stream-Adresse, die auf seiner Kommandozeile steht, behält " "aber Vorrang." ), }), ("Live-Bild", { "fields": ("live_url", "live_enabled", "live_bildrate"), "description": _( "Das Livebild kommt vom Live-Verteiler des Detectors, nicht " "von der Kamera selbst — die liefert nur einen Stream, und den " "braucht die Erkennung. Starten mit " "python -m detector watch … --live-port 8131." ), }), ("Technisches", { "fields": ("token_hash", "created_at", "last_seen", "letzter_kontakt"), "classes": ("collapse",), }), ) @admin.display(description="Zustand") def health(self, obj: Camera) -> str: if not obj.active: return "stillgelegt" return "meldet sich nicht" if obj.is_stale else "ok" @admin.display(description="Live", boolean=False) def live(self, obj: Camera) -> str: """Warum das Livebild (nicht) erscheint — geraten wird hier nicht.""" if not settings.LIVE_ALLOWED: return _("gesperrt (VOGELALBUM_LIVE_ERLAUBT=0)") if not obj.live_enabled: return _("aus") if not obj.live_url: return _("an, aber keine Adresse") if not obj.active: return _("an, aber Kamera stillgelegt") return _("an") @admin.register(Species) class SpeciesAdmin(admin.ModelAdmin): list_display = ("german", "scientific", "rarity", "xp", "size_cm") list_filter = ("rarity",) search_fields = ("german", "scientific", "english") prepopulated_fields = {"slug": ("german",)} @admin.register(Sighting) class SightingAdmin(admin.ModelAdmin): list_display = ( "started_at", "camera", "species", "confirmed_species", "confidence_pct", "thumb", ) list_filter = ("camera", "species", "is_first_of_species") date_hierarchy = "started_at" readonly_fields = ("preview", "votes", "xp_awarded", "is_first_of_species") # Handkorrektur ist der häufigste Grund, hier reinzuschauen — deshalb direkt # aus der Liste heraus editierbar. list_editable = ("confirmed_species",) @admin.display(description="Sicherheit", ordering="confidence") def confidence_pct(self, obj: Sighting) -> str: return f"{obj.confidence:.0%}" @admin.display(description="Bild") def thumb(self, obj: Sighting): if not obj.crop: return "—" return format_html('', obj.crop.url) @admin.display(description="Vorschau") def preview(self, obj: Sighting): if not obj.image: return "—" return format_html('', obj.image.url) @admin.register(DexEntry) class DexEntryAdmin(admin.ModelAdmin): list_display = ("species", "count", "first_seen", "last_seen") readonly_fields = ("species", "first_seen", "last_seen", "count", "best_sighting") @admin.register(Badge) class BadgeAdmin(admin.ModelAdmin): list_display = ("emoji", "name", "code", "xp", "sort") @admin.register(EarnedBadge) class EarnedBadgeAdmin(admin.ModelAdmin): list_display = ("badge", "earned_at") @admin.register(Benachrichtigung) class BenachrichtigungAdmin(admin.ModelAdmin): """Ein einziger Datensatz — deshalb kein Hinzufügen und kein Löschen.""" fieldsets = ( (None, {"fields": ("aktiv",)}), ("Wohin", {"fields": ("server", "thema")}), ("Was", {"fields": ("sichtungen", "stoerungen", "stille_minuten")}), ) actions = ["probe_senden"] def has_add_permission(self, request): # Der Datensatz wird beim ersten Aufruf angelegt, siehe changelist_view. return False def has_delete_permission(self, request, obj=None): return False def changelist_view(self, request, extra_context=None): # Sicherstellen, dass es den einen Eintrag gibt — sonst steht hier # eine leere Liste und niemand weiss, wo man anfaengt. Benachrichtigung.laden() return super().changelist_view(request, extra_context) @admin.action(description="Probemeldung senden") def probe_senden(self, request, queryset): """⚠ Wartet bewusst auf die Antwort. Eine Probe, die still im Hintergrund scheitert, taugt nichts — man will ja gerade wissen, ob die Einstellungen stimmen.""" geklappt, text = bn.melde_probe() if geklappt: self.message_user(request, f"Probemeldung: {text}", messages.SUCCESS) else: self.message_user(request, f"Probemeldung fehlgeschlagen: {text}", messages.ERROR)