16428f2c16
Models, AI assistant, and UI now show diskussionsfertige Antworten without titles; shared search partial fixes aria-label rendering. Co-authored-by: Cursor <cursoragent@cursor.com>
233 lines
7.9 KiB
Python
233 lines
7.9 KiB
Python
from django.contrib import admin
|
|
from django.urls import path
|
|
from django.utils.html import format_html
|
|
|
|
from .admin_forms import (
|
|
ClaimAdminForm,
|
|
PossibleAnswerInlineForm,
|
|
EvidenceFileInlineForm,
|
|
SourceInlineForm,
|
|
)
|
|
from .admin_views import AssistantView, ReviewDraftView
|
|
from .models import Category, Claim, ClaimDraft, PossibleAnswer, EvidenceFile, Source, Tag
|
|
|
|
|
|
class FaktenAdminMixin:
|
|
class Media:
|
|
css = {'all': ['css/admin_custom.css']}
|
|
js = ['js/admin_custom.js']
|
|
|
|
|
|
class EvidenceFileInline(FaktenAdminMixin, admin.StackedInline):
|
|
model = EvidenceFile
|
|
form = EvidenceFileInlineForm
|
|
extra = 1
|
|
fields = ('preview', 'file', 'title', 'caption', 'order')
|
|
readonly_fields = ('preview',)
|
|
classes = ('collapse-open',)
|
|
verbose_name = 'Nachweis'
|
|
verbose_name_plural = 'Nachweise (Grafiken / PDFs)'
|
|
|
|
@admin.display(description='Vorschau')
|
|
def preview(self, obj):
|
|
if not obj or not obj.pk or not obj.file:
|
|
return format_html('<span class="quiet">Nach dem Speichern sichtbar</span>')
|
|
if obj.is_image:
|
|
return format_html(
|
|
'<div class="fk-evidence-preview"><a href="{}" target="_blank" rel="noopener">'
|
|
'<img src="{}" alt="{}"></a></div>',
|
|
obj.file.url,
|
|
obj.file.url,
|
|
obj.image_alt,
|
|
)
|
|
if obj.is_pdf:
|
|
return format_html(
|
|
'<div class="fk-evidence-preview"><a href="{}" target="_blank" rel="noopener">'
|
|
'📄 PDF öffnen</a></div>',
|
|
obj.file.url,
|
|
)
|
|
return '—'
|
|
|
|
|
|
class SourceInline(FaktenAdminMixin, admin.StackedInline):
|
|
model = Source
|
|
form = SourceInlineForm
|
|
extra = 1
|
|
fields = ('title', 'organization', 'url', 'description')
|
|
classes = ('collapse-open',)
|
|
verbose_name = 'Quelle'
|
|
verbose_name_plural = 'Quellen'
|
|
|
|
|
|
class PossibleAnswerInline(FaktenAdminMixin, admin.StackedInline):
|
|
model = PossibleAnswer
|
|
form = PossibleAnswerInlineForm
|
|
extra = 1
|
|
fields = ('answer',)
|
|
classes = ('collapse-open',)
|
|
verbose_name = 'Mögliche Antwort'
|
|
verbose_name_plural = 'Mögliche Antworten'
|
|
|
|
|
|
@admin.register(Tag)
|
|
class TagAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
|
list_display = ('name', 'slug')
|
|
prepopulated_fields = {'slug': ('name',)}
|
|
search_fields = ('name',)
|
|
|
|
|
|
@admin.register(Category)
|
|
class CategoryAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
|
list_display = ('name', 'slug', 'claim_count', 'created_at')
|
|
prepopulated_fields = {'slug': ('name',)}
|
|
search_fields = ('name', 'description')
|
|
fieldsets = (
|
|
(None, {
|
|
'fields': ('name', 'slug', 'icon'),
|
|
}),
|
|
('Beschreibung', {
|
|
'fields': ('description',),
|
|
'classes': ('wide',),
|
|
}),
|
|
)
|
|
|
|
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
if db_field.name == 'description':
|
|
formfield.widget.attrs.update({'rows': 4, 'class': 'fk-admin-textarea'})
|
|
return formfield
|
|
|
|
@admin.display(description='Anzahl Behauptungen')
|
|
def claim_count(self, obj):
|
|
return obj.claims.count()
|
|
|
|
|
|
@admin.register(Claim)
|
|
class ClaimAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
|
form = ClaimAdminForm
|
|
change_form_template = 'admin/claims/claim/change_form.html'
|
|
list_display = ('title', 'category', 'status', 'updated_at')
|
|
list_display_links = ('title',)
|
|
list_filter = ('category', 'status', 'tags')
|
|
search_fields = ('title', 'short_answer', 'detailed_explanation')
|
|
prepopulated_fields = {'slug': ('title',)}
|
|
filter_horizontal = ('tags',)
|
|
autocomplete_fields = ('category',)
|
|
save_on_top = True
|
|
date_hierarchy = 'updated_at'
|
|
inlines = [EvidenceFileInline, SourceInline, PossibleAnswerInline]
|
|
readonly_fields = ('public_link', 'created_at', 'updated_at')
|
|
fieldsets = (
|
|
('Einordnung', {
|
|
'fields': ('category', 'status', 'tags'),
|
|
}),
|
|
('Behauptung', {
|
|
'fields': ('title', 'slug', 'public_link'),
|
|
}),
|
|
('Texte', {
|
|
'fields': ('short_answer', 'detailed_explanation'),
|
|
'classes': ('wide',),
|
|
'description': 'Kurzantwort: in ~20 Sekunden lesbar. Ausführliche Erklärung optional.',
|
|
}),
|
|
('Zeitstempel', {
|
|
'fields': ('created_at', 'updated_at'),
|
|
'classes': ('collapse',),
|
|
}),
|
|
)
|
|
|
|
@admin.display(description='Website')
|
|
def public_link(self, obj):
|
|
if not obj.pk:
|
|
return '—'
|
|
return format_html(
|
|
'<a href="{}" target="_blank" rel="noopener">Behauptung auf der Website öffnen</a>',
|
|
obj.get_absolute_url(),
|
|
)
|
|
|
|
|
|
@admin.register(EvidenceFile)
|
|
class EvidenceFileAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
|
list_display = ('preview_thumb', 'display_title', 'claim', 'order', 'created_at')
|
|
list_filter = ('claim__category',)
|
|
search_fields = ('title', 'caption', 'claim__title')
|
|
readonly_fields = ('preview',)
|
|
fields = ('claim', 'preview', 'file', 'title', 'caption', 'order')
|
|
|
|
@admin.display(description='')
|
|
def preview_thumb(self, obj):
|
|
if obj.is_image and obj.file:
|
|
return format_html(
|
|
'<img src="{}" style="max-width:48px;max-height:48px;object-fit:contain">',
|
|
obj.file.url,
|
|
)
|
|
return obj.file_extension.upper()
|
|
|
|
@admin.display(description='Vorschau')
|
|
def preview(self, obj):
|
|
return EvidenceFileInline.preview(self, obj)
|
|
|
|
|
|
@admin.register(Source)
|
|
class SourceAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
|
form = SourceInlineForm
|
|
list_display = ('title', 'organization', 'claim')
|
|
list_filter = ('claim__category',)
|
|
search_fields = ('title', 'organization', 'description')
|
|
|
|
|
|
@admin.register(PossibleAnswer)
|
|
class PossibleAnswerAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
|
form = PossibleAnswerInlineForm
|
|
list_display = ('answer_preview', 'claim')
|
|
list_filter = ('claim__category',)
|
|
search_fields = ('answer', 'claim__title')
|
|
|
|
@admin.display(description='Antwort')
|
|
def answer_preview(self, obj):
|
|
return obj.answer[:80]
|
|
|
|
|
|
@admin.register(ClaimDraft)
|
|
class ClaimDraftAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
|
change_list_template = 'admin/claims/claimdraft/change_list.html'
|
|
list_display = ('title', 'status', 'created_by', 'created_at', 'published_claim')
|
|
list_filter = ('status', 'created_at')
|
|
search_fields = ('title', 'user_notes', 'article_url')
|
|
readonly_fields = (
|
|
'created_by', 'status', 'article_url', 'user_notes', 'uploaded_file',
|
|
'article_text', 'conversation', 'ai_raw_response', 'error_message',
|
|
'published_claim', 'created_at', 'updated_at',
|
|
)
|
|
|
|
def get_queryset(self, request):
|
|
qs = super().get_queryset(request)
|
|
if request.user.is_superuser:
|
|
return qs
|
|
return qs.filter(created_by=request.user)
|
|
|
|
def has_change_permission(self, request, obj=None):
|
|
if obj and not request.user.is_superuser and obj.created_by_id != request.user.id:
|
|
return False
|
|
return super().has_change_permission(request, obj)
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
if obj and not request.user.is_superuser and obj.created_by_id != request.user.id:
|
|
return False
|
|
return super().has_delete_permission(request, obj)
|
|
|
|
def get_urls(self):
|
|
urls = super().get_urls()
|
|
custom_urls = [
|
|
path(
|
|
'assistant/',
|
|
self.admin_site.admin_view(AssistantView.as_view()),
|
|
name='claims_claimdraft_assistant',
|
|
),
|
|
path(
|
|
'<int:draft_id>/review/',
|
|
self.admin_site.admin_view(ReviewDraftView.as_view()),
|
|
name='claims_claimdraft_review',
|
|
),
|
|
]
|
|
return custom_urls + urls
|