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:
+124
-1
@@ -1,3 +1,7 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import FileExtensionValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
@@ -5,6 +9,7 @@ from django.utils.text import slugify
|
||||
|
||||
EVIDENCE_FILE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf']
|
||||
IMAGE_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'}
|
||||
DRAFT_UPLOAD_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf']
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
@@ -143,7 +148,11 @@ class Claim(models.Model):
|
||||
|
||||
|
||||
def evidence_upload_path(instance, filename):
|
||||
return f'evidence/{instance.claim.slug}/{filename}'
|
||||
ext = os.path.splitext(filename)[1].lower().lstrip('.') or 'bin'
|
||||
if ext not in EVIDENCE_FILE_EXTENSIONS:
|
||||
ext = 'bin'
|
||||
claim_id = instance.claim_id or 'new'
|
||||
return f'evidence/{claim_id}/{uuid.uuid4().hex[:12]}.{ext}'
|
||||
|
||||
|
||||
class EvidenceFile(models.Model):
|
||||
@@ -157,6 +166,7 @@ class EvidenceFile(models.Model):
|
||||
file = models.FileField(
|
||||
'Datei',
|
||||
upload_to=evidence_upload_path,
|
||||
max_length=500,
|
||||
validators=[FileExtensionValidator(allowed_extensions=EVIDENCE_FILE_EXTENSIONS)],
|
||||
help_text='Bilder (JPG, PNG, GIF, WebP, SVG) oder PDF',
|
||||
)
|
||||
@@ -190,6 +200,10 @@ class EvidenceFile(models.Model):
|
||||
def is_pdf(self):
|
||||
return self.file_extension == 'pdf'
|
||||
|
||||
@property
|
||||
def image_alt(self):
|
||||
return self.caption or 'Nachweis-Grafik'
|
||||
|
||||
|
||||
class Source(models.Model):
|
||||
claim = models.ForeignKey(
|
||||
@@ -229,3 +243,112 @@ class CounterArgument(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return self.argument[:80]
|
||||
|
||||
|
||||
def draft_upload_path(instance, filename):
|
||||
return f'drafts/{instance.pk or "new"}/{filename}'
|
||||
|
||||
|
||||
class ClaimDraft(models.Model):
|
||||
class DraftStatus(models.TextChoices):
|
||||
PROCESSING = 'processing', 'Wird verarbeitet'
|
||||
REVIEW = 'review', 'Zur Prüfung'
|
||||
PUBLISHED = 'published', 'Veröffentlicht'
|
||||
FAILED = 'failed', 'Fehlgeschlagen'
|
||||
REJECTED = 'rejected', 'Abgelehnt'
|
||||
|
||||
created_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='claim_drafts',
|
||||
verbose_name='Erstellt von',
|
||||
)
|
||||
status = models.CharField(
|
||||
'Status',
|
||||
max_length=20,
|
||||
choices=DraftStatus.choices,
|
||||
default=DraftStatus.PROCESSING,
|
||||
)
|
||||
article_url = models.URLField('Artikel-URL', max_length=500, blank=True)
|
||||
user_notes = models.TextField('Eingabe / Beschreibung', blank=True)
|
||||
uploaded_file = models.FileField(
|
||||
'Hochgeladene Datei',
|
||||
upload_to='drafts/uploads/%Y/%m/',
|
||||
max_length=500,
|
||||
blank=True,
|
||||
validators=[FileExtensionValidator(allowed_extensions=DRAFT_UPLOAD_EXTENSIONS)],
|
||||
help_text='Optional: Bild oder PDF als Kontext',
|
||||
)
|
||||
article_text = models.TextField('Extrahierter Artikeltext', blank=True)
|
||||
|
||||
category_slug = models.SlugField(
|
||||
'Bestehende Kategorie (Slug)',
|
||||
max_length=220,
|
||||
blank=True,
|
||||
help_text='Slug einer vorhandenen Kategorie, falls passend',
|
||||
)
|
||||
new_category_name = models.CharField('Neue Kategorie', max_length=200, blank=True)
|
||||
new_category_description = models.TextField('Beschreibung neue Kategorie', blank=True)
|
||||
new_category_icon = models.CharField(
|
||||
'Icon neue Kategorie',
|
||||
max_length=50,
|
||||
blank=True,
|
||||
help_text='Bootstrap-Icon-Klasse, z. B. bi-thermometer-half',
|
||||
)
|
||||
|
||||
title = models.CharField('Behauptung', max_length=300, blank=True)
|
||||
claim_status = models.CharField(
|
||||
'Einordnung',
|
||||
max_length=40,
|
||||
choices=Claim.Status.choices,
|
||||
blank=True,
|
||||
)
|
||||
short_answer = models.TextField('Kurzantwort', blank=True)
|
||||
detailed_explanation = models.TextField('Ausführliche Erklärung', blank=True)
|
||||
evidence_level = models.CharField(
|
||||
'Evidenzlevel',
|
||||
max_length=20,
|
||||
choices=Claim.EvidenceLevel.choices,
|
||||
blank=True,
|
||||
)
|
||||
tags = models.JSONField('Schlagwörter', default=list, blank=True)
|
||||
sources = models.JSONField('Quellen', default=list, blank=True)
|
||||
counter_arguments = models.JSONField('Gegenargumente', default=list, blank=True)
|
||||
conversation = models.JSONField('Konversation', default=list, blank=True)
|
||||
ai_raw_response = models.JSONField('KI-Rohantwort', blank=True, null=True)
|
||||
error_message = models.TextField('Fehlermeldung', blank=True)
|
||||
published_claim = models.ForeignKey(
|
||||
Claim,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='origin_drafts',
|
||||
verbose_name='Veröffentlichte Behauptung',
|
||||
)
|
||||
created_at = models.DateTimeField('Erstellt am', auto_now_add=True)
|
||||
updated_at = models.DateTimeField('Aktualisiert am', auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Fakten-Entwurf'
|
||||
verbose_name_plural = 'Fakten-Entwürfe'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return self.title or f'Entwurf #{self.pk}'
|
||||
|
||||
def get_admin_review_url(self):
|
||||
return reverse('admin:claims_claimdraft_review', args=[self.pk])
|
||||
|
||||
def get_admin_assistant_url(self):
|
||||
return reverse('admin:claims_claimdraft_assistant')
|
||||
|
||||
def add_message(self, role, content):
|
||||
from django.utils import timezone
|
||||
|
||||
messages = list(self.conversation or [])
|
||||
messages.append({
|
||||
'role': role,
|
||||
'content': content,
|
||||
'at': timezone.now().isoformat(),
|
||||
})
|
||||
self.conversation = messages
|
||||
|
||||
Reference in New Issue
Block a user