Faktenkompass: UI-Rebrand, Admin-Assistent und Produktions-Setup.
Modernisiert die öffentliche Oberfläche mit Inter, Dark Mode und Live-Suche, ergänzt KI-gestützte Entwürfe im Admin mit Nachweis-Uploads und dokumentiert Daphne/Nginx für den Serverbetrieb. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+165
-17
@@ -1,36 +1,101 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.utils.html import format_html
|
||||
|
||||
from .models import Category, Claim, CounterArgument, EvidenceFile, Source, Tag
|
||||
from .admin_forms import (
|
||||
ClaimAdminForm,
|
||||
CounterArgumentInlineForm,
|
||||
EvidenceFileInlineForm,
|
||||
SourceInlineForm,
|
||||
)
|
||||
from .admin_views import AssistantView, ReviewDraftView
|
||||
from .models import Category, Claim, ClaimDraft, CounterArgument, EvidenceFile, Source, Tag
|
||||
|
||||
|
||||
class EvidenceFileInline(admin.TabularInline):
|
||||
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 = ('title', 'file', 'caption', 'order')
|
||||
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(admin.TabularInline):
|
||||
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 CounterArgumentInline(admin.StackedInline):
|
||||
class CounterArgumentInline(FaktenAdminMixin, admin.StackedInline):
|
||||
model = CounterArgument
|
||||
form = CounterArgumentInlineForm
|
||||
extra = 1
|
||||
fields = ('argument', 'response')
|
||||
classes = ('collapse-open',)
|
||||
verbose_name = 'Gegenargument'
|
||||
verbose_name_plural = 'Gegenargumente'
|
||||
|
||||
|
||||
@admin.register(Tag)
|
||||
class TagAdmin(admin.ModelAdmin):
|
||||
class TagAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
||||
list_display = ('name', 'slug')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
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):
|
||||
@@ -38,43 +103,81 @@ class CategoryAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(Claim)
|
||||
class ClaimAdmin(admin.ModelAdmin):
|
||||
class ClaimAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
||||
form = ClaimAdminForm
|
||||
change_form_template = 'admin/claims/claim/change_form.html'
|
||||
list_display = ('title', 'category', 'status', 'evidence_level', 'updated_at')
|
||||
list_display_links = ('title',)
|
||||
list_filter = ('category', 'status', 'evidence_level', '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, CounterArgumentInline]
|
||||
readonly_fields = ('public_link', 'created_at', 'updated_at')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('category', 'title', 'slug', 'status', 'evidence_level', 'tags'),
|
||||
('Einordnung', {
|
||||
'fields': ('category', 'status', 'evidence_level', 'tags'),
|
||||
}),
|
||||
('Inhalt', {
|
||||
('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(admin.ModelAdmin):
|
||||
list_display = ('display_title', 'claim', 'file_extension_display', 'order', 'created_at')
|
||||
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='Typ')
|
||||
def file_extension_display(self, obj):
|
||||
@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(admin.ModelAdmin):
|
||||
class SourceAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
||||
form = SourceInlineForm
|
||||
list_display = ('title', 'organization', 'claim')
|
||||
list_filter = ('claim__category',)
|
||||
search_fields = ('title', 'organization', 'description')
|
||||
|
||||
|
||||
@admin.register(CounterArgument)
|
||||
class CounterArgumentAdmin(admin.ModelAdmin):
|
||||
class CounterArgumentAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
||||
form = CounterArgumentInlineForm
|
||||
list_display = ('argument_preview', 'claim')
|
||||
list_filter = ('claim__category',)
|
||||
search_fields = ('argument', 'response')
|
||||
@@ -82,3 +185,48 @@ class CounterArgumentAdmin(admin.ModelAdmin):
|
||||
@admin.display(description='Gegenargument')
|
||||
def argument_preview(self, obj):
|
||||
return obj.argument[: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
|
||||
|
||||
Reference in New Issue
Block a user