from django.contrib import admin, messages
from django.shortcuts import redirect
from django.urls import path
from django.utils.html import format_html
from animetix.containers import get_container
from animetix.models import (
AIFeedback,
AIREvalResult,
AISafetyEvent,
BossParticipation,
GlobalBoss,
GoldDatasetEntry,
SiteConfiguration,
)
@admin.register(SiteConfiguration)
class SiteConfigurationAdmin(admin.ModelAdmin):
"""Réglages globaux (mode maintenance). Singleton : ni ajout ni suppression."""
list_display = ("__str__", "maintenance_mode", "maintenance_until", "updated_at")
def has_add_permission(self, request):
# Une seule ligne : on la crée à la volée au premier accès.
return not SiteConfiguration.objects.exists()
def has_delete_permission(self, request, obj=None):
return False
@admin.register(GlobalBoss)
class GlobalBossAdmin(admin.ModelAdmin):
"""Pilotage manuel des raids World Boss (le spawn hebdo est automatique)."""
list_display = (
"title",
"secret_title",
"media_type",
"current_hp",
"total_hp",
"current_phase",
"is_active",
"end_date",
)
list_filter = ("is_active", "media_type")
search_fields = ("title", "secret_title")
@admin.register(BossParticipation)
class BossParticipationAdmin(admin.ModelAdmin):
list_display = ("user", "boss", "points_contributed", "last_participation")
list_filter = ("boss",)
@admin.register(GoldDatasetEntry)
class GoldDatasetEntryAdmin(admin.ModelAdmin):
change_list_template = "admin/golddatasetentry_changelist.html"
list_display = (
"id",
"entry_type",
"instruction_preview",
"is_validated",
"ai_validation_score_display",
"confidence_score_display",
"is_safe_display",
"created_at",
"validation_status_display",
)
list_editable = ("is_validated",)
list_filter = ("is_validated", "entry_type", "is_safe", "created_at")
search_fields = ("instruction", "context", "response", "ai_critique")
readonly_fields = (
"created_at",
"source_feedback",
"ai_validation_score",
"confidence_score",
"is_safe",
)
actions = ["validate_selected", "invalidate_selected", "promote_selected_entries"]
fieldsets = (
(
"Données Synthétiques",
{
"fields": (
"entry_type",
"instruction",
"context",
"response",
"metadata",
)
},
),
(
"Validation IA (HITL Gate)",
{
"fields": (
"ai_validation_score",
"confidence_score",
"is_safe",
"ai_critique",
),
"description": "Résultats de la validation croisée automatique.",
},
),
(
"Statut Humain",
{"fields": ("is_validated", "source_feedback", "created_at")},
),
)
@admin.display(description="Instruction")
def instruction_preview(self, obj):
return (
obj.instruction[:80] + "..."
if len(obj.instruction) > 80
else obj.instruction
)
@admin.display(description="Contexte")
def context_preview(self, obj):
return obj.context[:80] + "..." if len(obj.context) > 80 else obj.context
@admin.display(description="Score IA", ordering="ai_validation_score")
def ai_validation_score_display(self, obj):
color = (
"green"
if obj.ai_validation_score >= 0.7
else "orange" if obj.ai_validation_score >= 0.4 else "red"
)
return format_html(
'{:.2f}',
color,
obj.ai_validation_score,
)
@admin.display(description="Confiance XAI", ordering="confidence_score")
def confidence_score_display(self, obj):
color = (
"green"
if obj.confidence_score >= 0.7
else "orange" if obj.confidence_score >= 0.4 else "red"
)
return format_html(
'{:.2f}',
color,
obj.confidence_score,
)
@admin.display(description="Sécurité")
def is_safe_display(self, obj):
if obj.is_safe:
return format_html('✔ Safe')
return format_html(
'✖ UNSAFE'
)
@admin.display(description="Statut")
def validation_status_display(self, obj):
from django.utils.safestring import mark_safe # noqa: E402
if obj.is_validated:
return mark_safe(
'✔ Validé'
) # nosec B308
return mark_safe(
'⏳ En attente'
) # nosec B308
@admin.action(description="Valider les entrées sélectionnées")
def validate_selected(self, request, queryset):
rows_updated = queryset.update(is_validated=True)
if rows_updated == 1:
message_bit = "1 entrée a été validée."
else:
message_bit = f"{rows_updated} entrées ont été validées."
self.message_user(request, f"Succès : {message_bit}", messages.SUCCESS)
@admin.action(description="Invalider les entrées sélectionnées")
def invalidate_selected(self, request, queryset):
rows_updated = queryset.update(is_validated=False)
if rows_updated == 1:
message_bit = "1 entrée a été invalidée."
else:
message_bit = f"{rows_updated} entrées ont été invalidées."
self.message_user(request, f"Succès : {message_bit}", messages.SUCCESS)
@admin.action(description="Promouvoir les entrées sélectionnées validées")
def promote_selected_entries(self, request, queryset):
validated_subset = queryset.filter(is_validated=True)
if not validated_subset.exists():
self.message_user(
request,
"Erreur : Aucune des entrées sélectionnées n'est validée (is_validated=True). Seules les entrées validées peuvent être promues.",
messages.WARNING,
)
return
container = get_container()
promotion_service = container.core.synthetic_promotion_service()
try:
stats = promotion_service.promote_validated_entries()
count = stats.get("promoted", 0)
details = stats.get("details", {})
self.message_user(
request,
f"Promotion réussie de {count} entrées. Détails: {details}",
messages.SUCCESS,
)
except Exception as e:
self.message_user(
request, f"Erreur lors de la promotion : {str(e)}", messages.ERROR
)
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
"promote/",
self.admin_site.admin_view(self.promote_validated_view),
name="animetix_golddatasetentry_promote",
),
]
return custom_urls + urls
def promote_validated_view(self, request):
container = get_container()
promotion_service = container.core.synthetic_promotion_service()
try:
stats = promotion_service.promote_validated_entries()
count = stats.get("promoted", 0)
details = stats.get("details", {})
msg = f"Succès : {count} entrées validées ont été promues et exportées. Détails : {details}"
self.message_user(request, msg, messages.SUCCESS)
except Exception as e:
self.message_user(
request,
f"Erreur lors de la promotion globale : {str(e)}",
messages.ERROR,
)
return redirect("../")
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
# Calcul des statistiques interactives
extra_context["stats"] = {
"pending_qa": GoldDatasetEntry.objects.filter(
is_validated=False, entry_type="QA"
).count(),
"pending_multiverse": GoldDatasetEntry.objects.filter(
is_validated=False, entry_type="MULTIVERSE"
).count(),
"pending_distillation": GoldDatasetEntry.objects.filter(
is_validated=False, entry_type="DISTILLATION"
).count(),
"pending_other": GoldDatasetEntry.objects.filter(
is_validated=False, entry_type="OTHER"
).count(),
"total_validated": GoldDatasetEntry.objects.filter(
is_validated=True
).count(),
}
return super().changelist_view(request, extra_context=extra_context)
@admin.register(AIFeedback)
class AIFeedbackAdmin(admin.ModelAdmin):
list_display = (
"id",
"user",
"feedback_type",
"is_positive",
"created_at",
"input_context_truncated",
)
list_filter = ("is_positive", "feedback_type", "created_at")
search_fields = ("input_context", "output_text", "user__username")
readonly_fields = ("created_at",)
@admin.display(description="Prompt/Contexte")
def input_context_truncated(self, obj):
return (
obj.input_context[:100] + "..."
if len(obj.input_context) > 100
else obj.input_context
)
@admin.register(AISafetyEvent)
class AISafetyEventAdmin(admin.ModelAdmin):
list_display = (
"id",
"event_type",
"action",
"detected_categories",
"created_at",
"user",
)
list_filter = ("event_type", "action", "created_at")
search_fields = ("input_text", "output_text", "reasoning", "user__username")
readonly_fields = ("created_at",)
@admin.register(AIREvalResult)
class AIREvalResultAdmin(admin.ModelAdmin):
list_display = (
"id",
"game_mode",
"faithfulness",
"relevancy",
"precision",
"hallucination_detected",
"created_at",
)
list_filter = ("game_mode", "hallucination_detected", "created_at")
search_fields = ("input_context", "output_text")
readonly_fields = ("created_at",)