Compare commits
3 Commits
fce11689e1
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 16428f2c16 | |||
| 28aab3b16a | |||
| 3a5849660a |
@@ -1,6 +1,6 @@
|
||||
# Faktenkompass – Wissensdatenbank für überprüfbare Behauptungen
|
||||
|
||||
Eine Django-Webplattform zum schnellen Nachschlagen von Fakten, Quellen und Gegenargumenten zu häufigen Aussagen in Diskussionen.
|
||||
Eine Django-Webplattform zum schnellen Nachschlagen von Fakten, Quellen und Antworten zu häufigen Aussagen in Diskussionen.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
@@ -42,7 +42,7 @@ python manage.py migrate
|
||||
python manage.py load_sample_data
|
||||
```
|
||||
|
||||
Dies erstellt 7 Kategorien mit insgesamt 15 Behauptungen, Quellen und Gegenargumenten.
|
||||
Dies erstellt 7 Kategorien mit insgesamt 15 Behauptungen, Quellen und möglichen Antworten.
|
||||
|
||||
### 5. Admin-Zugang einrichten
|
||||
|
||||
@@ -117,11 +117,24 @@ cd /pfad/zu/ArgumentAtlas
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
set -a && source /etc/faktenkompass.env && set +a
|
||||
python manage.py migrate
|
||||
python manage.py collectstatic --noinput
|
||||
python manage.py createsuperuser # falls noch nicht vorhanden
|
||||
```
|
||||
|
||||
**Wichtig:** `collectstatic` immer mit denselben Umgebungsvariablen ausführen wie der Daphne-Dienst (`DJANGO_DEBUG=False`). Sonst fehlen gehashte Dateinamen bzw. `staticfiles.json` und die Seite antwortet mit 500.
|
||||
|
||||
Nach jedem `git pull` mit geänderten Static-Dateien:
|
||||
|
||||
```bash
|
||||
set -a && source /etc/faktenkompass.env && set +a
|
||||
python manage.py collectstatic --clear --noinput
|
||||
sudo systemctl restart faktenkompass
|
||||
```
|
||||
|
||||
Erfolg prüfen: `staticfiles/staticfiles.json` existiert, CSS heißt z. B. `custom.<hash>.css`.
|
||||
|
||||
Stellen Sie sicher, dass der Prozessnutzer Schreibrechte für `media/` und die Datenbank hat.
|
||||
|
||||
### 3. Daphne manuell testen
|
||||
@@ -242,7 +255,7 @@ ArgumentAtlas/
|
||||
│ ├── settings.py
|
||||
│ └── urls.py
|
||||
├── claims/ # Haupt-App
|
||||
│ ├── models.py # Category, Claim, Source, EvidenceFile, CounterArgument, Tag
|
||||
│ ├── models.py # Category, Claim, Source, EvidenceFile, PossibleAnswer, Tag
|
||||
│ ├── views.py
|
||||
│ ├── urls.py
|
||||
│ ├── admin.py
|
||||
@@ -257,14 +270,14 @@ ArgumentAtlas/
|
||||
|
||||
- **Startseite:** Kategorien als Karten mit Behauptungsanzahl
|
||||
- **Kategorieseite:** Behauptungen filtern und durchsuchen
|
||||
- **Behauptungsdetail:** Status, Kurzantwort, Nachweise (Bilder/PDFs), Evidenzlevel, Quellen, Gegenargumente, Tags
|
||||
- **Behauptungsdetail:** Status, Kurzantwort, Nachweise (Bilder/PDFs), Quellen, mögliche Antworten, Tags
|
||||
- **Globale Suche:** Behauptungen über alle Kategorien hinweg
|
||||
- **Admin:** Alle Inhalte über Django Admin pflegen
|
||||
- **SEO:** Sprechende URLs (`/kategorie/klimawandel/`, `/behauptung/...`)
|
||||
|
||||
## Inhalte verwalten
|
||||
|
||||
Melden Sie sich im Django-Admin an, um Kategorien, Behauptungen, Quellen, Gegenargumente und Schlagwörter anzulegen oder zu bearbeiten. Slugs werden automatisch aus Titeln generiert.
|
||||
Melden Sie sich im Django-Admin an, um Kategorien, Behauptungen, Quellen, mögliche Antworten und Schlagwörter anzulegen oder zu bearbeiten. Slugs werden automatisch aus Titeln generiert.
|
||||
|
||||
### Nachweise hochladen
|
||||
|
||||
@@ -292,7 +305,7 @@ export OPENAI_API_KEY=sk-...
|
||||
|
||||
1. Admin öffnen → **Fakten-Assistent** (Startseite oder unter Fakten-Entwürfe)
|
||||
2. Artikel-URL, Beschreibung und/oder Bild/PDF eingeben
|
||||
3. **Entwurf generieren** – die KI schlägt Kategorie, Behauptung, Quellen und Gegenargumente vor
|
||||
3. **Entwurf generieren** – die KI schlägt Kategorie, Behauptung, Quellen und mögliche Antworten vor
|
||||
4. Entwurf prüfen, bearbeiten, ggf. **KI erneut fragen**
|
||||
5. Erst mit **Veröffentlichen** wird der Eintrag live geschaltet
|
||||
|
||||
|
||||
+20
-20
@@ -4,12 +4,12 @@ from django.utils.html import format_html
|
||||
|
||||
from .admin_forms import (
|
||||
ClaimAdminForm,
|
||||
CounterArgumentInlineForm,
|
||||
PossibleAnswerInlineForm,
|
||||
EvidenceFileInlineForm,
|
||||
SourceInlineForm,
|
||||
)
|
||||
from .admin_views import AssistantView, ReviewDraftView
|
||||
from .models import Category, Claim, ClaimDraft, CounterArgument, EvidenceFile, Source, Tag
|
||||
from .models import Category, Claim, ClaimDraft, PossibleAnswer, EvidenceFile, Source, Tag
|
||||
|
||||
|
||||
class FaktenAdminMixin:
|
||||
@@ -59,14 +59,14 @@ class SourceInline(FaktenAdminMixin, admin.StackedInline):
|
||||
verbose_name_plural = 'Quellen'
|
||||
|
||||
|
||||
class CounterArgumentInline(FaktenAdminMixin, admin.StackedInline):
|
||||
model = CounterArgument
|
||||
form = CounterArgumentInlineForm
|
||||
class PossibleAnswerInline(FaktenAdminMixin, admin.StackedInline):
|
||||
model = PossibleAnswer
|
||||
form = PossibleAnswerInlineForm
|
||||
extra = 1
|
||||
fields = ('argument', 'response')
|
||||
fields = ('answer',)
|
||||
classes = ('collapse-open',)
|
||||
verbose_name = 'Gegenargument'
|
||||
verbose_name_plural = 'Gegenargumente'
|
||||
verbose_name = 'Mögliche Antwort'
|
||||
verbose_name_plural = 'Mögliche Antworten'
|
||||
|
||||
|
||||
@admin.register(Tag)
|
||||
@@ -106,20 +106,20 @@ class CategoryAdmin(FaktenAdminMixin, 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 = ('title', 'category', 'status', 'updated_at')
|
||||
list_display_links = ('title',)
|
||||
list_filter = ('category', 'status', 'evidence_level', 'tags')
|
||||
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, CounterArgumentInline]
|
||||
inlines = [EvidenceFileInline, SourceInline, PossibleAnswerInline]
|
||||
readonly_fields = ('public_link', 'created_at', 'updated_at')
|
||||
fieldsets = (
|
||||
('Einordnung', {
|
||||
'fields': ('category', 'status', 'evidence_level', 'tags'),
|
||||
'fields': ('category', 'status', 'tags'),
|
||||
}),
|
||||
('Behauptung', {
|
||||
'fields': ('title', 'slug', 'public_link'),
|
||||
@@ -175,16 +175,16 @@ class SourceAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
||||
search_fields = ('title', 'organization', 'description')
|
||||
|
||||
|
||||
@admin.register(CounterArgument)
|
||||
class CounterArgumentAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
||||
form = CounterArgumentInlineForm
|
||||
list_display = ('argument_preview', 'claim')
|
||||
@admin.register(PossibleAnswer)
|
||||
class PossibleAnswerAdmin(FaktenAdminMixin, admin.ModelAdmin):
|
||||
form = PossibleAnswerInlineForm
|
||||
list_display = ('answer_preview', 'claim')
|
||||
list_filter = ('claim__category',)
|
||||
search_fields = ('argument', 'response')
|
||||
search_fields = ('answer', 'claim__title')
|
||||
|
||||
@admin.display(description='Gegenargument')
|
||||
def argument_preview(self, obj):
|
||||
return obj.argument[:80]
|
||||
@admin.display(description='Antwort')
|
||||
def answer_preview(self, obj):
|
||||
return obj.answer[:80]
|
||||
|
||||
|
||||
@admin.register(ClaimDraft)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
|
||||
from .models import Claim, CounterArgument, EvidenceFile, Source
|
||||
from .models import Claim, PossibleAnswer, EvidenceFile, Source
|
||||
|
||||
|
||||
class ClaimAdminForm(forms.ModelForm):
|
||||
@@ -26,13 +26,12 @@ class SourceInlineForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class CounterArgumentInlineForm(forms.ModelForm):
|
||||
class PossibleAnswerInlineForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CounterArgument
|
||||
model = PossibleAnswer
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
'argument': forms.Textarea(attrs={'rows': 3, 'class': 'fk-admin-textarea'}),
|
||||
'response': forms.Textarea(attrs={'rows': 4, 'class': 'fk-admin-textarea'}),
|
||||
'answer': forms.Textarea(attrs={'rows': 4, 'class': 'fk-admin-textarea'}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
+9
-11
@@ -53,8 +53,8 @@ class ReviewDraftForm(forms.ModelForm):
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={'class': 'form-control font-monospace', 'rows': 6}),
|
||||
)
|
||||
counter_arguments_json = forms.CharField(
|
||||
label='Gegenargumente (JSON)',
|
||||
possible_answers_json = forms.CharField(
|
||||
label='Mögliche Antworten (JSON)',
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={'class': 'form-control font-monospace', 'rows': 6}),
|
||||
)
|
||||
@@ -73,7 +73,6 @@ class ReviewDraftForm(forms.ModelForm):
|
||||
'claim_status',
|
||||
'short_answer',
|
||||
'detailed_explanation',
|
||||
'evidence_level',
|
||||
'new_category_name',
|
||||
'new_category_description',
|
||||
'new_category_icon',
|
||||
@@ -83,7 +82,6 @@ class ReviewDraftForm(forms.ModelForm):
|
||||
'claim_status': forms.Select(attrs={'class': 'form-select'}),
|
||||
'short_answer': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
|
||||
'detailed_explanation': forms.Textarea(attrs={'class': 'form-control', 'rows': 6}),
|
||||
'evidence_level': forms.Select(attrs={'class': 'form-select'}),
|
||||
'new_category_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'new_category_description': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
||||
'new_category_icon': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'bi-book'}),
|
||||
@@ -97,8 +95,8 @@ class ReviewDraftForm(forms.ModelForm):
|
||||
self.fields['sources_json'].initial = json.dumps(
|
||||
instance.sources or [], ensure_ascii=False, indent=2
|
||||
)
|
||||
self.fields['counter_arguments_json'].initial = json.dumps(
|
||||
instance.counter_arguments or [], ensure_ascii=False, indent=2
|
||||
self.fields['possible_answers_json'].initial = json.dumps(
|
||||
instance.possible_answers or [], ensure_ascii=False, indent=2
|
||||
)
|
||||
if instance.category_slug:
|
||||
category = Category.objects.filter(slug=instance.category_slug).first()
|
||||
@@ -121,23 +119,23 @@ class ReviewDraftForm(forms.ModelForm):
|
||||
raise forms.ValidationError('Quellen müssen eine JSON-Liste sein.')
|
||||
return data
|
||||
|
||||
def clean_counter_arguments_json(self):
|
||||
raw = (self.cleaned_data.get('counter_arguments_json') or '').strip()
|
||||
def clean_possible_answers_json(self):
|
||||
raw = (self.cleaned_data.get('possible_answers_json') or '').strip()
|
||||
if not raw:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise forms.ValidationError('Ungültiges JSON für Gegenargumente.') from exc
|
||||
raise forms.ValidationError('Ungültiges JSON für mögliche Antworten.') from exc
|
||||
if not isinstance(data, list):
|
||||
raise forms.ValidationError('Gegenargumente müssen eine JSON-Liste sein.')
|
||||
raise forms.ValidationError('Mögliche Antworten müssen eine JSON-Liste sein.')
|
||||
return data
|
||||
|
||||
def save(self, commit=True):
|
||||
draft = super().save(commit=False)
|
||||
draft.tags = self.cleaned_data['tags_text']
|
||||
draft.sources = self.cleaned_data['sources_json']
|
||||
draft.counter_arguments = self.cleaned_data['counter_arguments_json']
|
||||
draft.possible_answers = self.cleaned_data['possible_answers_json']
|
||||
category = self.cleaned_data.get('category_choice')
|
||||
if category:
|
||||
draft.category_slug = category.slug
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from claims.models import Category, Claim, CounterArgument, Source, Tag
|
||||
from claims.models import Category, Claim, PossibleAnswer, Source, Tag
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -87,7 +87,6 @@ class Command(BaseCommand):
|
||||
'Sie bleibt im Zytoplasma und wird nicht in den Zellkern transportiert, wo die DNA liegt. '
|
||||
'Integration in das Erbgut wäre ohne spezielle Hilfsmechanismen, die Impfstoffe nicht enthalten, praktisch nicht möglich.'
|
||||
),
|
||||
evidence_level=Claim.EvidenceLevel.SEHR_STARK,
|
||||
)
|
||||
c1.tags.add(tags['impfstoff'], tags['gesundheit'])
|
||||
Source.objects.create(
|
||||
@@ -97,10 +96,9 @@ class Command(BaseCommand):
|
||||
url='https://www.rki.de/DE/Content/InfAZ/N/Neuartiges_Coronavirus/Impfen/Impfen_node.html',
|
||||
description='Offizielle Informationen des RKI zu Wirkweise und Sicherheit der Impfstoffe.',
|
||||
)
|
||||
CounterArgument.objects.create(
|
||||
PossibleAnswer.objects.create(
|
||||
claim=c1,
|
||||
argument='Die mRNA ist künstlich und kann deshalb die Zelle dauerhaft verändern.',
|
||||
response='Künstliche mRNA unterscheidet sich in der Wirkung nicht grundlegend von natürlicher mRNA im Körper. Beide werden abgebaut. Es gibt keinen Mechanismus, durch den Impf-mRNA in die DNA integriert wird.',
|
||||
answer='Künstliche mRNA unterscheidet sich in der Wirkung nicht grundlegend von natürlicher mRNA im Körper. Beide werden abgebaut. Es gibt keinen Mechanismus, durch den Impf-mRNA in die DNA integriert wird.',
|
||||
)
|
||||
|
||||
c2 = Claim.objects.create(
|
||||
@@ -109,7 +107,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.FALSCH,
|
||||
short_answer='In den ersten Pandemiejahren war COVID-19 deutlich tödlicher als saisonale Influenza und führte weltweit zu Millionen überzähliger Todesfälle.',
|
||||
detailed_explanation='Vergleiche mit der Grippe vereinfachen oft: COVID-19 verursachte höhere Belastungen für Intensivstationen und wies in vulnerablen Gruppen eine höhere Letalität auf.',
|
||||
evidence_level=Claim.EvidenceLevel.STARK,
|
||||
)
|
||||
c2.tags.add(tags['gesundheit'])
|
||||
Source.objects.create(
|
||||
@@ -126,7 +123,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.WISSENSCHAFTLICHER_KONSENS,
|
||||
short_answer='Gut sitzende medizinische Masken und FFP2-Masken reduzieren nachweislich die Übertragung von Tröpfchen und Aerosolen, besonders in Innenräumen.',
|
||||
detailed_explanation='Metaanalysen zeigen, dass Masken die Emission und Aufnahme infektiöser Partikel verringern. Der Schutz hängt von Maskentyp, Passform und Nutzung ab.',
|
||||
evidence_level=Claim.EvidenceLevel.STARK,
|
||||
)
|
||||
|
||||
def _create_climate_claims(self, category, tags):
|
||||
@@ -136,7 +132,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.FALSCH,
|
||||
short_answer='Die aktuelle Erwärmung lässt sich nicht durch natürliche Zyklen allein erklären. Treibhausgasemissionen sind laut IPCC die dominierende Ursache.',
|
||||
detailed_explanation='Klimamodelle mit nur natürlichen Faktoren reproduzieren die gemessene Temperaturanomalie nicht. Mit anthropogenen Emissionen stimmen Beobachtungen und Modelle überein.',
|
||||
evidence_level=Claim.EvidenceLevel.SEHR_STARK,
|
||||
)
|
||||
c1.tags.add(tags['klima'], tags['co2'])
|
||||
Source.objects.create(
|
||||
@@ -146,10 +141,9 @@ class Command(BaseCommand):
|
||||
url='https://www.ipcc.ch/report/ar6/syr/',
|
||||
description='Aktueller Synthesebericht des Weltklimarats.',
|
||||
)
|
||||
CounterArgument.objects.create(
|
||||
PossibleAnswer.objects.create(
|
||||
claim=c1,
|
||||
argument='Früher gab es auch warme und kalte Perioden ohne Industrie.',
|
||||
response='Natürliche Schwankungen sind bekannt, aber die Geschwindigkeit der aktuellen Erwärmung passt zu Treibhausgasanstieg, nicht zu dominanten natürlichen Zyklen.',
|
||||
answer='Natürliche Schwankungen sind bekannt, aber die Geschwindigkeit der aktuellen Erwärmung passt zu Treibhausgasanstieg, nicht zu dominanten natürlichen Zyklen.',
|
||||
)
|
||||
|
||||
Claim.objects.create(
|
||||
@@ -158,7 +152,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.FALSCH,
|
||||
short_answer='Über 97 % der klimabezogenen Fachpublikationen stützen den Konsens, dass der Mensch die Hauptursache der aktuellen Erwärmung ist.',
|
||||
detailed_explanation='Große wissenschaftliche Gesellschaften weltweit befürworten diese Einschätzung.',
|
||||
evidence_level=Claim.EvidenceLevel.SEHR_STARK,
|
||||
)
|
||||
|
||||
Claim.objects.create(
|
||||
@@ -167,7 +160,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.TEILWEISE_RICHTIG,
|
||||
short_answer='Viele Extremwetterarten zeigen einen Zusammenhang mit dem Klimawandel, aber nicht jedes Ereignis ist direkt darauf zurückzuführen.',
|
||||
detailed_explanation='Hitzewellen und starke Niederschläge werden in vielen Regionen häufiger. Attributionsforschung berechnet Wahrscheinlichkeitsveränderungen für Einzelereignisse.',
|
||||
evidence_level=Claim.EvidenceLevel.STARK,
|
||||
)
|
||||
|
||||
def _create_energy_claims(self, category, tags):
|
||||
@@ -177,13 +169,11 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.TEILWEISE_RICHTIG,
|
||||
short_answer='Langfristig ist ein sehr hoher Anteil erneuerbarer Energien möglich, erfordert aber Speicher, Netzausbau und Flexibilität.',
|
||||
detailed_explanation='Studien zeigen Szenarien mit nahezu 100 % erneuerbarem Strom, erfordern aber Infrastruktur und Sektorkopplung.',
|
||||
evidence_level=Claim.EvidenceLevel.MITTEL,
|
||||
)
|
||||
c1.tags.add(tags['erneuerbare'])
|
||||
CounterArgument.objects.create(
|
||||
PossibleAnswer.objects.create(
|
||||
claim=c1,
|
||||
argument='Wenn die Sonne nicht scheint und der Wind nicht weht, fällt das Netz aus.',
|
||||
response='Energiesysteme kombinieren Speicher, Netze, Importe und Reservekapazitäten statt auf eine einzige Quelle zu setzen.',
|
||||
answer='Energiesysteme kombinieren Speicher, Netze, Importe und Reservekapazitäten statt auf eine einzige Quelle zu setzen.',
|
||||
)
|
||||
|
||||
Claim.objects.create(
|
||||
@@ -192,7 +182,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.IRREFUEHREND,
|
||||
short_answer='Atomkraft erzeugt wenig CO₂ im Betrieb, verursacht aber andere Risiken und hohe Folgekosten. Sie ist nicht die einzige klimafreundliche Option.',
|
||||
detailed_explanation='Lebenszyklusanalysen zeigen niedrige CO₂-Emissionen, aber Endlagerung und Unfallrisiken sind zentrale Gegenargumente.',
|
||||
evidence_level=Claim.EvidenceLevel.STARK,
|
||||
)
|
||||
|
||||
Claim.objects.create(
|
||||
@@ -201,7 +190,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.IRREFUEHREND,
|
||||
short_answer='Strompreise werden von Brennstoffkosten, Netzentgelten und Abgaben beeinflusst. Erneuerbare senken Grenzkosten, verursachen aber Netzausbaukosten.',
|
||||
detailed_explanation='In Phasen hoher Gaspreise stiegen Börsenstrompreise stark. Langfristig sind Wind und Solar kostengünstige Neubau-Technologien.',
|
||||
evidence_level=Claim.EvidenceLevel.MITTEL,
|
||||
)
|
||||
|
||||
def _create_migration_claims(self, category, tags):
|
||||
@@ -211,7 +199,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.IRREFUEHREND,
|
||||
short_answer='Die Erwerbstätigenquote variiert stark nach Herkunft und Aufenthaltsdauer. Pauschale Aussagen sind nicht haltbar.',
|
||||
detailed_explanation='Viele Zuwanderer arbeiten nach mehreren Jahren ähnlich häufig wie die Gesamtbevölkerung. Neuankömmlinge weisen anfangs häufiger Arbeitslosigkeit auf.',
|
||||
evidence_level=Claim.EvidenceLevel.STARK,
|
||||
)
|
||||
c1.tags.add(tags['migration'])
|
||||
Source.objects.create(
|
||||
@@ -228,7 +215,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.TEILWEISE_RICHTIG,
|
||||
short_answer='Deutschland nimmt absolut viele Zuwanderer auf, liegt pro Kopf aber nicht immer an der Spitze in Europa.',
|
||||
detailed_explanation='Eurostat-Daten zeigen Deutschland unter den Ländern mit höchsten absoluten Zahlen. Relativ zur Bevölkerung liegen andere Länder teils höher.',
|
||||
evidence_level=Claim.EvidenceLevel.STARK,
|
||||
)
|
||||
|
||||
def _create_economy_claims(self, category, tags):
|
||||
@@ -238,7 +224,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.IRREFUEHREND,
|
||||
short_answer='Lohnkosten können Preise mitverursachen, aber Energiepreise, Lieferketten und Geldpolitik spielen ebenfalls eine zentrale Rolle.',
|
||||
detailed_explanation='Die Inflation 2022/2023 wurde maßgeblich durch Energie- und Rohstoffpreisschocks getrieben.',
|
||||
evidence_level=Claim.EvidenceLevel.STARK,
|
||||
)
|
||||
|
||||
Claim.objects.create(
|
||||
@@ -247,7 +232,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.FALSCH,
|
||||
short_answer='Deutschland ist ein führender Exporteur, lag in den letzten Jahren aber hinter China.',
|
||||
detailed_explanation='Nach WTO-Daten exportierte China mehr Waren als Deutschland. Deutschland bleibt stark im Maschinen- und Fahrzeugbau.',
|
||||
evidence_level=Claim.EvidenceLevel.SEHR_STARK,
|
||||
)
|
||||
|
||||
def _create_politics_claims(self, category, tags):
|
||||
@@ -257,7 +241,6 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.TEILWEISE_RICHTIG,
|
||||
short_answer='Bei Bundestagswahlen schwankt die Beteiligung; langfristig gibt es einen leichten Rückgang, aber keine einheitliche Abwärtsspirale.',
|
||||
detailed_explanation='Historische Daten zeigen Schwankungen zwischen etwa 70 % und 90 % bei Bundestagswahlen.',
|
||||
evidence_level=Claim.EvidenceLevel.STARK,
|
||||
)
|
||||
|
||||
def _create_science_claims(self, category, tags):
|
||||
@@ -267,11 +250,9 @@ class Command(BaseCommand):
|
||||
status=Claim.Status.FALSCH,
|
||||
short_answer='Einzelstudien sind Ausgangspunkte, kein abschließender Beweis. Konsens entsteht durch Replikation und Synthese vieler Arbeiten.',
|
||||
detailed_explanation='Peer Review filtert Fehler, garantiert aber keine Wahrheit. Metaanalysen fassen viele Studien zusammen.',
|
||||
evidence_level=Claim.EvidenceLevel.SEHR_STARK,
|
||||
)
|
||||
c1.tags.add(tags['forschung'])
|
||||
CounterArgument.objects.create(
|
||||
PossibleAnswer.objects.create(
|
||||
claim=c1,
|
||||
argument='Wenn eine Studie in einer renommierten Zeitschrift steht, ist sie bewiesen.',
|
||||
response='Renommierte Journale publizieren auch widerlegte Ergebnisse. Die Qualität hängt von Replikation und Methodik ab.',
|
||||
answer='Renommierte Journale publizieren auch widerlegte Ergebnisse. Die Qualität hängt von Replikation und Methodik ab.',
|
||||
)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-07 20:10
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('claims', '0005_allow_svg_draft_upload'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='claim',
|
||||
name='evidence_level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='claimdraft',
|
||||
name='evidence_level',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,69 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def migrate_possible_answer_text(apps, schema_editor):
|
||||
PossibleAnswer = apps.get_model('claims', 'PossibleAnswer')
|
||||
for obj in PossibleAnswer.objects.all():
|
||||
obj.answer = (obj.response or obj.argument or '').strip()
|
||||
obj.save(update_fields=['answer'])
|
||||
|
||||
|
||||
def migrate_draft_json(apps, schema_editor):
|
||||
ClaimDraft = apps.get_model('claims', 'ClaimDraft')
|
||||
for draft in ClaimDraft.objects.all():
|
||||
answers = []
|
||||
for item in draft.counter_arguments or []:
|
||||
if isinstance(item, str):
|
||||
text = item.strip()
|
||||
else:
|
||||
text = (item.get('answer') or item.get('response') or '').strip()
|
||||
if text:
|
||||
answers.append({'answer': text})
|
||||
draft.possible_answers = answers
|
||||
draft.save(update_fields=['possible_answers'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('claims', '0006_remove_claim_evidence_level_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='CounterArgument',
|
||||
new_name='PossibleAnswer',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='possibleanswer',
|
||||
name='answer',
|
||||
field=models.TextField(blank=True, default='', verbose_name='Antwort'),
|
||||
),
|
||||
migrations.RunPython(migrate_possible_answer_text, migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name='possibleanswer',
|
||||
name='argument',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='possibleanswer',
|
||||
name='response',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='possibleanswer',
|
||||
options={
|
||||
'ordering': ['id'],
|
||||
'verbose_name': 'Mögliche Antwort',
|
||||
'verbose_name_plural': 'Mögliche Antworten',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='claimdraft',
|
||||
name='possible_answers',
|
||||
field=models.JSONField(blank=True, default=list, verbose_name='Mögliche Antworten'),
|
||||
),
|
||||
migrations.RunPython(migrate_draft_json, migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name='claimdraft',
|
||||
name='counter_arguments',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-07 20:47
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('claims', '0007_possible_answers'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='possibleanswer',
|
||||
name='answer',
|
||||
field=models.TextField(verbose_name='Antwort'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='possibleanswer',
|
||||
name='claim',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='possible_answers', to='claims.claim', verbose_name='Behauptung'),
|
||||
),
|
||||
]
|
||||
+7
-35
@@ -72,12 +72,6 @@ class Claim(models.Model):
|
||||
WISSENSCHAFTLICHER_KONSENS = 'wissenschaftlicher_konsens', 'Wissenschaftlicher Konsens'
|
||||
OFFENE_FRAGE = 'offene_frage', 'Offene Frage'
|
||||
|
||||
class EvidenceLevel(models.TextChoices):
|
||||
SEHR_STARK = 'sehr_stark', 'Sehr stark'
|
||||
STARK = 'stark', 'Stark'
|
||||
MITTEL = 'mittel', 'Mittel'
|
||||
SCHWACH = 'schwach', 'Schwach'
|
||||
|
||||
category = models.ForeignKey(
|
||||
Category,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -94,12 +88,6 @@ class Claim(models.Model):
|
||||
)
|
||||
short_answer = models.TextField('Kurzantwort')
|
||||
detailed_explanation = models.TextField('Ausführliche Erklärung', blank=True)
|
||||
evidence_level = models.CharField(
|
||||
'Evidenzlevel',
|
||||
max_length=20,
|
||||
choices=EvidenceLevel.choices,
|
||||
default=EvidenceLevel.MITTEL,
|
||||
)
|
||||
tags = models.ManyToManyField(Tag, blank=True, verbose_name='Schlagwörter')
|
||||
created_at = models.DateTimeField('Erstellt am', auto_now_add=True)
|
||||
updated_at = models.DateTimeField('Aktualisiert am', auto_now=True)
|
||||
@@ -137,15 +125,6 @@ class Claim(models.Model):
|
||||
}
|
||||
return mapping.get(self.status, 'secondary')
|
||||
|
||||
def get_evidence_badge_class(self):
|
||||
mapping = {
|
||||
self.EvidenceLevel.SEHR_STARK: 'success',
|
||||
self.EvidenceLevel.STARK: 'primary',
|
||||
self.EvidenceLevel.MITTEL: 'info',
|
||||
self.EvidenceLevel.SCHWACH: 'warning',
|
||||
}
|
||||
return mapping.get(self.evidence_level, 'secondary')
|
||||
|
||||
|
||||
def evidence_upload_path(instance, filename):
|
||||
ext = os.path.splitext(filename)[1].lower().lstrip('.') or 'bin'
|
||||
@@ -226,23 +205,22 @@ class Source(models.Model):
|
||||
return self.title
|
||||
|
||||
|
||||
class CounterArgument(models.Model):
|
||||
class PossibleAnswer(models.Model):
|
||||
claim = models.ForeignKey(
|
||||
Claim,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='counter_arguments',
|
||||
related_name='possible_answers',
|
||||
verbose_name='Behauptung',
|
||||
)
|
||||
argument = models.TextField('Gegenargument')
|
||||
response = models.TextField('Antwort')
|
||||
answer = models.TextField('Antwort')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Gegenargument'
|
||||
verbose_name_plural = 'Gegenargumente'
|
||||
verbose_name = 'Mögliche Antwort'
|
||||
verbose_name_plural = 'Mögliche Antworten'
|
||||
ordering = ['id']
|
||||
|
||||
def __str__(self):
|
||||
return self.argument[:80]
|
||||
return self.answer[:80]
|
||||
|
||||
|
||||
def draft_upload_path(instance, filename):
|
||||
@@ -305,15 +283,9 @@ class ClaimDraft(models.Model):
|
||||
)
|
||||
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)
|
||||
possible_answers = models.JSONField('Mögliche Antworten', 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)
|
||||
|
||||
@@ -8,7 +8,6 @@ from django.conf import settings
|
||||
from claims.models import Category, Claim
|
||||
|
||||
VALID_CLAIM_STATUSES = {choice[0] for choice in Claim.Status.choices}
|
||||
VALID_EVIDENCE_LEVELS = {choice[0] for choice in Claim.EvidenceLevel.choices}
|
||||
|
||||
SYSTEM_PROMPT = """Du bist ein sachlicher Fakten-Assistent für die Plattform „Faktenkompass“.
|
||||
Deine Aufgabe: Aus Artikeln, Bildern und Nutzereingaben einen überprüfbaren Fakteneintrag erstellen.
|
||||
@@ -21,6 +20,7 @@ Regeln:
|
||||
- Quellen müssen echte, nachvollziehbare Referenzen sein (URL aus Eingabe bevorzugen).
|
||||
- Wähle eine passende bestehende Kategorie (use_existing_slug) ODER schlage eine neue vor.
|
||||
- Icons für neue Kategorien: Bootstrap Icons Klassen wie bi-thermometer-half, bi-virus, bi-lightning-charge.
|
||||
- Unter possible_answers: 2–4 kurze, diskussionsfertige Antworten auf die Behauptung formulieren.
|
||||
|
||||
Antworte ausschließlich als gültiges JSON mit dieser Struktur:
|
||||
{
|
||||
@@ -36,8 +36,7 @@ Antworte ausschließlich als gültiges JSON mit dieser Struktur:
|
||||
"title": "Die zu prüfende Behauptung",
|
||||
"status": "falsch|irrefuehrend|teilweise_richtig|unbelegt|wissenschaftlicher_konsens|offene_frage",
|
||||
"short_answer": "Kurze Einordnung",
|
||||
"detailed_explanation": "Ausführlichere Erklärung",
|
||||
"evidence_level": "sehr_stark|stark|mittel|schwach"
|
||||
"detailed_explanation": "Ausführlichere Erklärung"
|
||||
},
|
||||
"tags": ["Schlagwort1", "Schlagwort2"],
|
||||
"sources": [
|
||||
@@ -48,10 +47,9 @@ Antworte ausschließlich als gültiges JSON mit dieser Struktur:
|
||||
"description": "Kurzbeschreibung"
|
||||
}
|
||||
],
|
||||
"counter_arguments": [
|
||||
"possible_answers": [
|
||||
{
|
||||
"argument": "Häufiges Gegenargument",
|
||||
"response": "Sachliche Antwort"
|
||||
"answer": "Kurze, sachliche Antwort auf die Behauptung für die Diskussion"
|
||||
}
|
||||
],
|
||||
"assistant_summary": "Kurze Zusammenfassung für den Admin (1-2 Sätze)"
|
||||
@@ -180,11 +178,8 @@ def _call_openai(messages, *, has_image=False):
|
||||
def _validate_payload(payload):
|
||||
claim = payload.get('claim') or {}
|
||||
status = claim.get('status', '')
|
||||
evidence = claim.get('evidence_level', '')
|
||||
if status and status not in VALID_CLAIM_STATUSES:
|
||||
raise AIAssistantError(f'Ungültiger Status von der KI: {status}')
|
||||
if evidence and evidence not in VALID_EVIDENCE_LEVELS:
|
||||
raise AIAssistantError(f'Ungültiges Evidenzlevel von der KI: {evidence}')
|
||||
return payload
|
||||
|
||||
|
||||
@@ -234,6 +229,18 @@ def generate_draft_payload(
|
||||
return _validate_payload(payload)
|
||||
|
||||
|
||||
def _normalize_possible_answers(items):
|
||||
answers = []
|
||||
for item in items or []:
|
||||
if isinstance(item, str):
|
||||
text = item.strip()
|
||||
else:
|
||||
text = (item.get('answer') or item.get('response') or '').strip()
|
||||
if text:
|
||||
answers.append({'answer': text})
|
||||
return answers
|
||||
|
||||
|
||||
def apply_payload_to_draft(draft, payload):
|
||||
category = payload.get('category') or {}
|
||||
claim = payload.get('claim') or {}
|
||||
@@ -248,10 +255,11 @@ def apply_payload_to_draft(draft, payload):
|
||||
draft.claim_status = claim.get('status', Claim.Status.UNBELEGT)
|
||||
draft.short_answer = claim.get('short_answer', '')
|
||||
draft.detailed_explanation = claim.get('detailed_explanation', '')
|
||||
draft.evidence_level = claim.get('evidence_level', Claim.EvidenceLevel.MITTEL)
|
||||
draft.tags = payload.get('tags') or []
|
||||
draft.sources = payload.get('sources') or []
|
||||
draft.counter_arguments = payload.get('counter_arguments') or []
|
||||
draft.possible_answers = _normalize_possible_answers(
|
||||
payload.get('possible_answers', payload.get('counter_arguments'))
|
||||
)
|
||||
draft.ai_raw_response = payload
|
||||
summary = payload.get('assistant_summary', 'Entwurf wurde erstellt.')
|
||||
draft.add_message('assistant', summary)
|
||||
|
||||
@@ -4,7 +4,7 @@ import uuid
|
||||
from django.db import transaction
|
||||
from django.utils.text import slugify
|
||||
|
||||
from claims.models import Category, Claim, CounterArgument, EvidenceFile, Source, Tag
|
||||
from claims.models import Category, Claim, EvidenceFile, PossibleAnswer, Source, Tag
|
||||
|
||||
|
||||
class DraftPublishError(Exception):
|
||||
@@ -57,7 +57,6 @@ def publish_draft(draft):
|
||||
status=draft.claim_status or Claim.Status.UNBELEGT,
|
||||
short_answer=draft.short_answer,
|
||||
detailed_explanation=draft.detailed_explanation,
|
||||
evidence_level=draft.evidence_level or Claim.EvidenceLevel.MITTEL,
|
||||
)
|
||||
claim.tags.set(_get_or_create_tags(draft.tags))
|
||||
|
||||
@@ -73,11 +72,13 @@ def publish_draft(draft):
|
||||
description=source.get('description', ''),
|
||||
)
|
||||
|
||||
for counter in draft.counter_arguments or []:
|
||||
argument = (counter.get('argument') or '').strip()
|
||||
response = (counter.get('response') or '').strip()
|
||||
if argument and response:
|
||||
CounterArgument.objects.create(claim=claim, argument=argument, response=response)
|
||||
for item in draft.possible_answers or []:
|
||||
if isinstance(item, str):
|
||||
answer = item.strip()
|
||||
else:
|
||||
answer = (item.get('answer') or item.get('response') or '').strip()
|
||||
if answer:
|
||||
PossibleAnswer.objects.create(claim=claim, answer=answer)
|
||||
|
||||
if draft.uploaded_file:
|
||||
_attach_uploaded_file(draft, claim)
|
||||
|
||||
@@ -9,12 +9,6 @@ def status_label(value):
|
||||
return dict(Claim.Status.choices).get(value, value)
|
||||
|
||||
|
||||
@register.filter
|
||||
def evidence_label(value):
|
||||
from claims.models import Claim
|
||||
return dict(Claim.EvidenceLevel.choices).get(value, value)
|
||||
|
||||
|
||||
@register.filter
|
||||
def quoted(value):
|
||||
if not value:
|
||||
@@ -25,8 +19,3 @@ def quoted(value):
|
||||
@register.inclusion_tag('claims/partials/status_badge.html')
|
||||
def status_badge(claim):
|
||||
return {'claim': claim}
|
||||
|
||||
|
||||
@register.inclusion_tag('claims/partials/evidence_badge.html')
|
||||
def evidence_badge(claim):
|
||||
return {'claim': claim}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ def category_detail(request, slug):
|
||||
def claim_detail(request, slug):
|
||||
claim = get_object_or_404(
|
||||
Claim.objects.select_related('category').prefetch_related(
|
||||
'tags', 'sources', 'counter_arguments', 'evidence_files'
|
||||
'tags', 'sources', 'possible_answers', 'evidence_files'
|
||||
),
|
||||
slug=slug,
|
||||
)
|
||||
|
||||
+37
-2
@@ -252,6 +252,42 @@ a:hover .claim-card {
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
.fk-search .input-group-text,
|
||||
.fk-search .fk-search-input,
|
||||
.fk-search-filter {
|
||||
min-height: 2.625rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.fk-search .input-group-text,
|
||||
.fk-search .fk-search-input {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.fk-search .fk-search-input {
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
|
||||
.fk-search:focus-within .input-group-text,
|
||||
.fk-search:focus-within .fk-search-input {
|
||||
border-color: var(--fk-accent);
|
||||
}
|
||||
|
||||
.fk-search:focus-within {
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fk-accent) 18%, transparent);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.fk-search-filter {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.fk-search-form .col-sm-auto {
|
||||
min-width: 11rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--fk-accent);
|
||||
border-color: var(--fk-accent);
|
||||
@@ -306,8 +342,7 @@ a:hover .claim-card {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.fk-status,
|
||||
.fk-evidence {
|
||||
.fk-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% else %}
|
||||
<div class="fk-claim-help">
|
||||
Neue Behauptung anlegen: zuerst Titel und Kurzantwort ausfüllen, dann speichern.
|
||||
Nachweise, Quellen und Gegenargumente können danach ergänzt werden.
|
||||
Nachweise, Quellen und mögliche Antworten können danach ergänzt werden.
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
|
||||
@@ -81,11 +81,6 @@
|
||||
{{ form.claim_status }}
|
||||
</div>
|
||||
|
||||
<div class="field-box">
|
||||
<label for="{{ form.evidence_level.id_for_label }}">{{ form.evidence_level.label }}</label>
|
||||
{{ form.evidence_level }}
|
||||
</div>
|
||||
|
||||
<div class="field-box">
|
||||
<label for="{{ form.short_answer.id_for_label }}">{{ form.short_answer.label }}</label>
|
||||
{{ form.short_answer }}
|
||||
@@ -107,8 +102,8 @@
|
||||
</div>
|
||||
|
||||
<div class="field-box">
|
||||
<label for="{{ form.counter_arguments_json.id_for_label }}">{{ form.counter_arguments_json.label }}</label>
|
||||
{{ form.counter_arguments_json }}
|
||||
<label for="{{ form.possible_answers_json.id_for_label }}">{{ form.possible_answers_json.label }}</label>
|
||||
{{ form.possible_answers_json }}
|
||||
</div>
|
||||
|
||||
<div class="actions-bar">
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="row g-2 mb-4" data-live-search data-live-target="#category-results">
|
||||
<form method="get" class="row g-2 mb-4 fk-search-form" data-live-search data-live-target="#category-results">
|
||||
<div class="col-12 col-sm">
|
||||
<input type="search" id="q" name="q" class="form-control form-control-sm" placeholder="Suchen…" value="{{ query }}" autocomplete="off">
|
||||
{% include "claims/partials/search_input.html" with value=query aria_label="Behauptungen durchsuchen" %}
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto">
|
||||
<select id="status" name="status" class="form-select form-select-sm">
|
||||
<select id="status" name="status" class="form-select fk-search-filter">
|
||||
<option value="">Alle Status</option>
|
||||
{% for value, label in status_choices %}
|
||||
<option value="{{ value }}"{% if status == value %} selected{% endif %}>{{ label }}</option>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
<div class="fk-meta-stack mb-3">
|
||||
{% status_badge claim %}
|
||||
{% evidence_badge claim %}
|
||||
</div>
|
||||
|
||||
<div class="fk-card fk-answer-card">
|
||||
@@ -90,24 +89,13 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if claim.counter_arguments.all %}
|
||||
{% if claim.possible_answers.all %}
|
||||
<section class="fk-section">
|
||||
<p class="fk-section-label">Gegenargumente</p>
|
||||
<div class="accordion fk-accordion" id="counterArgs">
|
||||
{% for ca in claim.counter_arguments.all %}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed py-2 small" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapse{{ forloop.counter }}">
|
||||
{{ ca.argument }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse{{ forloop.counter }}" class="accordion-collapse collapse"
|
||||
data-bs-parent="#counterArgs">
|
||||
<div class="accordion-body py-2">
|
||||
{{ ca.response|linebreaks }}
|
||||
</div>
|
||||
</div>
|
||||
<p class="fk-section-label">Mögliche Antworten</p>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
{% for item in claim.possible_answers.all %}
|
||||
<div class="fk-card fk-section-body">
|
||||
{{ item.answer|linebreaks }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
<header class="mb-4">
|
||||
<h1 class="fk-page-title">Themen</h1>
|
||||
<form method="get" action="{% url 'claims:home' %}" class="mb-2" data-live-search data-live-target="#home-results">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text border-end-0"><i class="bi bi-search"></i></span>
|
||||
<input type="search" name="q" class="form-control border-start-0 ps-0" placeholder="Suchen…" value="{{ query }}" aria-label="Kategorien durchsuchen" autocomplete="off">
|
||||
</div>
|
||||
{% include "claims/partials/search_input.html" with value=query aria_label="Kategorien durchsuchen" %}
|
||||
</form>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="{% url 'claims:search' %}" class="small text-muted text-decoration-none">Alle Behauptungen</a>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<span class="fk-evidence">
|
||||
<i class="bi bi-bar-chart" aria-hidden="true"></i>
|
||||
Evidenz: {{ claim.get_evidence_level_display }}
|
||||
</span>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="input-group fk-search">
|
||||
<span class="input-group-text border-end-0"><i class="bi bi-search" aria-hidden="true"></i></span>
|
||||
<input type="search"
|
||||
name="{{ name|default:'q' }}"
|
||||
{% if id %}id="{{ id }}"{% else %}id="q"{% endif %}
|
||||
class="form-control fk-search-input border-start-0 ps-0"
|
||||
placeholder="{{ placeholder|default:'Suchen…' }}"
|
||||
value="{{ value|default:'' }}"
|
||||
{% if autofocus %}autofocus{% endif %}
|
||||
autocomplete="off"
|
||||
aria-label="{% firstof aria_label placeholder 'Suchen' %}">
|
||||
</div>
|
||||
@@ -5,13 +5,12 @@
|
||||
{% block content %}
|
||||
<h1 class="fk-page-title">Suche</h1>
|
||||
|
||||
<form method="get" action="{% url 'claims:search' %}" class="row g-2 mb-4" data-live-search data-live-target="#search-results">
|
||||
<form method="get" action="{% url 'claims:search' %}" class="row g-2 mb-4 fk-search-form" data-live-search data-live-target="#search-results">
|
||||
<div class="col-12">
|
||||
<input type="search" id="q" name="q" class="form-control form-control-sm"
|
||||
placeholder="Behauptung suchen…" value="{{ query }}" autofocus autocomplete="off">
|
||||
{% include "claims/partials/search_input.html" with value=query placeholder="Behauptung suchen…" autofocus=True aria_label="Behauptung suchen" %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<select id="kategorie" name="kategorie" class="form-select form-select-sm">
|
||||
<select id="kategorie" name="kategorie" class="form-select fk-search-filter">
|
||||
<option value="">Alle Themen</option>
|
||||
{% for cat in categories %}
|
||||
<option value="{{ cat.slug }}"{% if category_slug == cat.slug %} selected{% endif %}>{{ cat.name }}</option>
|
||||
@@ -19,7 +18,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<select id="status" name="status" class="form-select form-select-sm">
|
||||
<select id="status" name="status" class="form-select fk-search-filter">
|
||||
<option value="">Alle Status</option>
|
||||
{% for value, label in status_choices %}
|
||||
<option value="{{ value }}"{% if status == value %} selected{% endif %}>{{ label }}</option>
|
||||
|
||||
Reference in New Issue
Block a user