Coverage for baibu/chat/admin.py: 100%
125 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
1from django.contrib import admin
2from django.db import transaction
3from django.utils.translation import gettext_lazy as _
5from .models import AudioClip
6from .models import ChatFlag
7from .models import Conversation
8from .models import ConversationEvent
9from .models import ConversationTag
10from .models import Message
11from .models import ModelVariant
12from .models import Prompt
13from .models import Run
14from .models import ToolInvocation
15from .models import Topic
18class ReadOnlyInline(admin.TabularInline):
19 extra = 0
20 can_delete = False
21 show_change_link = False
23 def has_add_permission(self, request, obj=None):
24 return False
26 def has_change_permission(self, request, obj=None):
27 return False
30class MessageInline(ReadOnlyInline):
31 model = Message
32 fields = ("created_at", "role", "content")
33 readonly_fields = fields
36class RunInline(ReadOnlyInline):
37 model = Run
38 fields = ("created_at", "status", "attempt", "model_name", "prompt_version", "latency_ms", "error")
39 readonly_fields = fields
42class ToolInvocationInline(ReadOnlyInline):
43 model = ToolInvocation
44 fields = ("created_at", "tool_name", "provider", "status", "arguments", "result_count", "error", "latency_ms")
45 readonly_fields = fields
48class AudioClipInline(ReadOnlyInline):
49 """Metadata only: recordings are not played in the admin."""
51 model = AudioClip
52 fields = ("created_at", "status", "attempt", "content_type", "size_bytes", "duration_seconds", "provider", "error")
53 readonly_fields = fields
56class TagInline(ReadOnlyInline):
57 model = ConversationTag
58 fields = ("topic_list", "language", "intent", "tagger", "tagged_at", "error")
59 readonly_fields = fields
61 @admin.display(description=_("Topics"))
62 def topic_list(self, obj):
63 return ", ".join(topic.label for topic in obj.topics.all()) or "—"
66class EventInline(ReadOnlyInline):
67 model = ConversationEvent
68 fields = ("created_at", "type", "payload")
69 readonly_fields = fields
72@admin.register(Conversation)
73class ConversationAdmin(admin.ModelAdmin):
74 list_display = ("__str__", "user", "language_code", "consent_tier", "last_activity_at")
75 list_filter = ("consent_tier", "language_code", "tag__topics", "tag__intent")
76 search_fields = ("id", "user__email", "title")
77 date_hierarchy = "last_activity_at"
78 readonly_fields = ("id", "user", "title", "language_code", "consent_tier", "created_at", "last_activity_at")
79 inlines = (TagInline, MessageInline, AudioClipInline, RunInline, EventInline)
81 def has_add_permission(self, request):
82 return False
84 def has_change_permission(self, request, obj=None):
85 return request.method in {"GET", "HEAD"} and super().has_change_permission(request, obj)
88@admin.register(Run)
89class RunAdmin(admin.ModelAdmin):
90 list_display = ("id", "conversation", "status", "attempt", "model_name", "latency_ms", "created_at")
91 list_filter = ("status", "model_name")
92 date_hierarchy = "created_at"
93 readonly_fields = [field.name for field in Run._meta.fields]
94 inlines = (ToolInvocationInline,)
96 def has_add_permission(self, request):
97 return False
99 def has_change_permission(self, request, obj=None):
100 return request.method in {"GET", "HEAD"} and super().has_change_permission(request, obj)
103@admin.register(ModelVariant)
104class ModelVariantAdmin(admin.ModelAdmin):
105 list_display = ("name", "model", "api_base", "is_default")
106 fields = ("name", "model", "api_base", "api_key_env", "parameters", "is_default")
108 def save_model(self, request, obj, form, change):
109 with transaction.atomic():
110 if obj.is_default:
111 ModelVariant.objects.exclude(pk=obj.pk).update(is_default=False)
112 super().save_model(request, obj, form, change)
115@admin.register(Prompt)
116class PromptAdmin(admin.ModelAdmin):
117 """Adding a prompt creates the next version; existing versions cannot be edited."""
119 list_display = ("name", "version", "active", "notes", "created_by", "created_at")
120 list_filter = ("name", "active")
121 actions = ("activate",)
123 def get_fields(self, request, obj=None):
124 if obj is None:
125 return ("name", "body", "notes", "active")
126 return ("name", "version", "body", "notes", "active", "created_by", "created_at")
128 def get_readonly_fields(self, request, obj=None):
129 if obj is None:
130 return ()
131 return ("name", "version", "body", "notes", "created_by", "created_at")
133 def save_model(self, request, obj, form, change):
134 with transaction.atomic():
135 if not change:
136 obj.created_by = request.user
137 if obj.active:
138 Prompt.objects.filter(name=obj.name, active=True).exclude(pk=obj.pk).update(active=False)
139 super().save_model(request, obj, form, change)
141 @admin.action(description=_("Make the selected version active"))
142 def activate(self, request, queryset):
143 if queryset.count() != 1:
144 self.message_user(request, _("Select exactly one version."), level="warning")
145 return
146 prompt = queryset.get()
147 with transaction.atomic():
148 Prompt.objects.filter(name=prompt.name, active=True).update(active=False)
149 Prompt.objects.filter(pk=prompt.pk).update(active=True)
150 self.message_user(request, _("%(prompt)s is now active.") % {"prompt": prompt})
153@admin.register(ChatFlag)
154class ChatFlagAdmin(admin.ModelAdmin):
155 list_display = ("__str__", "conversation", "created_at", "decided_by")
156 list_filter = ("status", "reason")
157 readonly_fields = [field.name for field in ChatFlag._meta.fields]
159 def has_add_permission(self, request):
160 return False
162 def has_change_permission(self, request, obj=None):
163 return request.method in {"GET", "HEAD"} and super().has_change_permission(request, obj)
166@admin.register(Topic)
167class TopicAdmin(admin.ModelAdmin):
168 list_display = ("label", "slug", "active")
169 list_filter = ("active",)
170 prepopulated_fields = {"slug": ("label",)}